Introduce MvcRazorTemplateEngine

This commit is contained in:
Pranav K 2017-02-15 15:45:59 -08:00
parent 4faef7afaf
commit f7fd5114b3
30 changed files with 1030 additions and 1330 deletions

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
namespace Microsoft.AspNetCore.Mvc.Razor
{
/// <summary>
/// A <see cref="RazorTemplateEngine"/> for Mvc Razor views.
/// </summary>
public class MvcRazorTemplateEngine : RazorTemplateEngine
{
/// <summary>
/// Initializes a new instance of <see cref="MvcRazorTemplateEngine"/>.
/// </summary>
/// <param name="engine">The <see cref="RazorEngine"/>.</param>
/// <param name="project">The <see cref="RazorProject"/>.</param>
public MvcRazorTemplateEngine(
RazorEngine engine,
RazorProject project)
: base(engine, project)
{
Options.DefaultImports = GetDefaultImports();
}
/// <inheritsdoc />
public override RazorCodeDocument CreateCodeDocument(RazorProjectItem projectItem)
{
var codeDocument = base.CreateCodeDocument(projectItem);
codeDocument.SetRelativePath(projectItem.Path);
return codeDocument;
}
private static RazorSourceDocument GetDefaultImports()
{
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
writer.WriteLine("@using System");
writer.WriteLine("@using System.Linq");
writer.WriteLine("@using System.Collections.Generic");
writer.WriteLine("@using Microsoft.AspNetCore.Mvc");
writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering");
writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures");
writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel> Html");
writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json");
writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component");
writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url");
writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider");
writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor");
writer.Flush();
stream.Position = 0;
return RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8);
}
}
}
}

View File

@ -1,111 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Microsoft.AspNetCore.Mvc.Razor
{
/// <summary>
/// Contains methods to locate <c>_ViewStart.cshtml</c> and <c>_ViewImports.cshtml</c>
/// </summary>
public static class ViewHierarchyUtility
{
private const string ViewStartFileName = "_ViewStart.cshtml";
/// <summary>
/// File name of <c>_ViewImports.cshtml</c> file
/// </summary>
public static readonly string ViewImportsFileName = "_ViewImports.cshtml";
/// <summary>
/// Gets the view start locations that are applicable to the specified path.
/// </summary>
/// <param name="applicationRelativePath">The application relative path of the file to locate
/// <c>_ViewStart</c>s for.</param>
/// <returns>A sequence of paths that represent potential view start locations.</returns>
/// <remarks>
/// This method returns paths starting from the directory of <paramref name="applicationRelativePath"/> and
/// moves upwards until it hits the application root.
/// e.g.
/// /Views/Home/View.cshtml -> [ /Views/Home/_ViewStart.cshtml, /Views/_ViewStart.cshtml, /_ViewStart.cshtml ]
/// </remarks>
public static IEnumerable<string> GetViewStartLocations(string applicationRelativePath)
{
return GetHierarchicalPath(applicationRelativePath, ViewStartFileName);
}
/// <summary>
/// Gets the locations for <c>_ViewImports</c>s that are applicable to the specified path.
/// </summary>
/// <param name="applicationRelativePath">The application relative path of the file to locate
/// <c>_ViewImports</c>s for.</param>
/// <returns>A sequence of paths that represent potential <c>_ViewImports</c> locations.</returns>
/// <remarks>
/// This method returns paths starting from the directory of <paramref name="applicationRelativePath"/> and
/// moves upwards until it hits the application root.
/// e.g.
/// /Views/Home/View.cshtml -> [ /Views/Home/_ViewImports.cshtml, /Views/_ViewImports.cshtml,
/// /_ViewImports.cshtml ]
/// </remarks>
public static IEnumerable<string> GetViewImportsLocations(string applicationRelativePath)
{
return GetHierarchicalPath(applicationRelativePath, ViewImportsFileName);
}
private static IEnumerable<string> GetHierarchicalPath(string relativePath, string fileName)
{
if (string.IsNullOrEmpty(relativePath))
{
return Enumerable.Empty<string>();
}
if (relativePath.StartsWith("~/", StringComparison.Ordinal))
{
relativePath = relativePath.Substring(2);
}
if (relativePath.StartsWith("/", StringComparison.Ordinal))
{
relativePath = relativePath.Substring(1);
}
if (string.Equals(Path.GetFileName(relativePath), fileName, StringComparison.OrdinalIgnoreCase))
{
// If the specified path is for the file hierarchy being constructed, then the first file that applies
// to it is in a parent directory.
relativePath = Path.GetDirectoryName(relativePath);
if (string.IsNullOrEmpty(relativePath))
{
return Enumerable.Empty<string>();
}
}
var builder = new StringBuilder(relativePath);
builder.Replace('\\', '/');
if (builder.Length > 0 && builder[0] != '/')
{
builder.Insert(0, '/');
}
var locations = new List<string>();
for (var index = builder.Length - 1; index >= 0; index--)
{
if (builder[index] == '/')
{
builder.Length = index + 1;
builder.Append(fileName);
locations.Add(builder.ToString());
}
}
return locations;
}
}
}

View File

@ -1,21 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
/// <summary>
/// Specifies the contracts for a service that compiles Razor files.
/// </summary>
public interface IRazorCompilationService
{
/// <summary>
/// Compiles the razor file located at <paramref name="fileInfo"/>.
/// </summary>
/// <param name="fileInfo">A <see cref="RelativeFileInfo"/> instance that represents the file to compile.
/// </param>
/// <returns>
/// A <see cref="CompilationResult"/> that represents the results of parsing and compiling the file.
/// </returns>
CompilationResult Compile(RelativeFileInfo fileInfo);
}
}

View File

@ -1,46 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
/// <summary>
/// A container type that represents <see cref="IFileInfo"/> along with the application base relative path
/// for a file in the file system.
/// </summary>
public class RelativeFileInfo
{
/// <summary>
/// Initializes a new instance of <see cref="RelativeFileInfo"/>.
/// </summary>
/// <param name="fileInfo"><see cref="IFileInfo"/> for the file.</param>
/// <param name="relativePath">Path of the file relative to the application base.</param>
public RelativeFileInfo(IFileInfo fileInfo, string relativePath)
{
if (fileInfo == null)
{
throw new ArgumentNullException(nameof(fileInfo));
}
if (string.IsNullOrEmpty(relativePath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(relativePath));
}
FileInfo = fileInfo;
RelativePath = relativePath;
}
/// <summary>
/// Gets the <see cref="IFileInfo"/> associated with this instance of <see cref="RelativeFileInfo"/>.
/// </summary>
public IFileInfo FileInfo { get; }
/// <summary>
/// Gets the path of the file relative to the application base.
/// </summary>
public string RelativePath { get; }
}
}

View File

@ -170,12 +170,8 @@ namespace Microsoft.Extensions.DependencyInjection
// In the default scenario the following services are singleton by virtue of being initialized as part of
// creating the singleton RazorViewEngine instance.
services.TryAddTransient<IRazorPageFactoryProvider, DefaultRazorPageFactoryProvider>();
services.TryAddTransient<IRazorCompilationService, RazorCompilationService>();
services.TryAddSingleton<RazorProject>(s =>
{
return new DefaultRazorProject(s.GetRequiredService<IRazorViewEngineFileProviderAccessor>().FileProvider);
});
services.TryAddSingleton<RazorProject, DefaultRazorProject>();
services.TryAddSingleton<RazorEngine>(s =>
{

View File

@ -67,16 +67,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// <inheritdoc />
public CompilerCacheResult GetOrAdd(
string relativePath,
Func<RelativeFileInfo, CompilationResult> compile)
Func<string, CompilerCacheContext> cacheContextFactory)
{
if (relativePath == null)
{
throw new ArgumentNullException(nameof(relativePath));
}
if (compile == null)
if (cacheContextFactory == null)
{
throw new ArgumentNullException(nameof(compile));
throw new ArgumentNullException(nameof(cacheContextFactory));
}
Task<CompilerCacheResult> cacheEntry;
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var normalizedPath = GetNormalizedPath(relativePath);
if (!_cache.TryGetValue(normalizedPath, out cacheEntry))
{
cacheEntry = CreateCacheEntry(relativePath, normalizedPath, compile);
cacheEntry = CreateCacheEntry(normalizedPath, cacheContextFactory);
}
}
@ -97,14 +97,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
private Task<CompilerCacheResult> CreateCacheEntry(
string relativePath,
string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile)
Func<string, CompilerCacheContext> cacheContextFactory)
{
TaskCompletionSource<CompilerCacheResult> compilationTaskSource = null;
MemoryCacheEntryOptions cacheEntryOptions = null;
IFileInfo fileInfo = null;
MemoryCacheEntryOptions cacheEntryOptions;
Task<CompilerCacheResult> cacheEntry;
CompilerCacheContext compilerCacheContext;
// 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
@ -125,40 +124,39 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
throw new InvalidOperationException(message);
}
fileInfo = _fileProvider.GetFileInfo(normalizedPath);
if (!fileInfo.Exists)
{
var expirationToken = _fileProvider.Watch(normalizedPath);
cacheEntry = Task.FromResult(new CompilerCacheResult(new[] { expirationToken }));
cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.AddExpirationToken(expirationToken);
compilerCacheContext = cacheContextFactory(normalizedPath);
cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(compilerCacheContext.ProjectItem.Path));
if (!compilerCacheContext.ProjectItem.Exists)
{
cacheEntry = Task.FromResult(new CompilerCacheResult(normalizedPath, cacheEntryOptions.ExpirationTokens));
}
else
{
cacheEntryOptions = GetMemoryCacheEntryOptions(normalizedPath);
// A file exists and needs to be compiled.
compilationTaskSource = new TaskCompletionSource<CompilerCacheResult>();
foreach (var projectItem in compilerCacheContext.AdditionalCompilationItems)
{
cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(projectItem.Path));
}
cacheEntry = compilationTaskSource.Task;
}
cacheEntry = _cache.Set<Task<CompilerCacheResult>>(normalizedPath, cacheEntry, cacheEntryOptions);
cacheEntry = _cache.Set(normalizedPath, cacheEntry, cacheEntryOptions);
}
if (compilationTaskSource != null)
{
// Indicates that the file was found and needs to be compiled.
Debug.Assert(fileInfo != null && fileInfo.Exists);
// Indicates that a file was found and needs to be compiled.
Debug.Assert(cacheEntryOptions != null);
var relativeFileInfo = new RelativeFileInfo(fileInfo, normalizedPath);
try
{
var compilationResult = compile(relativeFileInfo);
var compilationResult = compilerCacheContext.Compile(compilerCacheContext);
compilationResult.EnsureSuccessful();
compilationTaskSource.SetResult(
new CompilerCacheResult(relativePath, compilationResult, cacheEntryOptions.ExpirationTokens));
new CompilerCacheResult(normalizedPath, compilationResult, cacheEntryOptions.ExpirationTokens));
}
catch (Exception ex)
{
@ -169,20 +167,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
return cacheEntry;
}
private MemoryCacheEntryOptions GetMemoryCacheEntryOptions(string relativePath)
{
var options = new MemoryCacheEntryOptions();
options.AddExpirationToken(_fileProvider.Watch(relativePath));
var viewImportsPaths = ViewHierarchyUtility.GetViewImportsLocations(relativePath);
foreach (var location in viewImportsPaths)
{
options.AddExpirationToken(_fileProvider.Watch(location));
}
return options;
}
private string GetNormalizedPath(string relativePath)
{
Debug.Assert(relativePath != null);

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public struct CompilerCacheContext
{
public CompilerCacheContext(
RazorProjectItem projectItem,
IEnumerable<RazorProjectItem> additionalCompilationItems,
Func<CompilerCacheContext, CompilationResult> compile)
{
ProjectItem = projectItem;
AdditionalCompilationItems = additionalCompilationItems;
Compile = compile;
}
public RazorProjectItem ProjectItem { get; }
public IEnumerable<RazorProjectItem> AdditionalCompilationItems { get; }
public Func<CompilerCacheContext, CompilationResult> Compile { get; }
}
}

View File

@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.Extensions.Primitives;
@ -18,21 +16,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="Compilation.CompilationResult"/>.
/// <see cref="CompilationResult"/>.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
public CompilerCacheResult(string relativePath, CompilationResult compilationResult)
: this(relativePath, compilationResult, EmptyArray<IChangeToken>.Instance)
{
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="Compilation.CompilationResult"/>.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
/// <param name="compilationResult">The <see cref="CompilationResult"/>.</param>
/// <param name="isPrecompiled"><c>true</c> if the view is precompiled, <c>false</c> otherwise.</param>
public CompilerCacheResult(string relativePath, CompilationResult compilationResult, bool isPrecompiled)
: this(relativePath, compilationResult, EmptyArray<IChangeToken>.Instance)
@ -42,10 +29,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="Compilation.CompilationResult"/>.
/// <see cref="CompilationResult"/>.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
/// <param name="compilationResult">The <see cref="CompilationResult"/>.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances that indicate when
/// this result has expired.</param>
public CompilerCacheResult(string relativePath, CompilationResult compilationResult, IList<IChangeToken> expirationTokens)
@ -55,18 +42,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
throw new ArgumentNullException(nameof(expirationTokens));
}
RelativePath = relativePath;
CompiledType = compilationResult.CompiledType;
ExpirationTokens = expirationTokens;
var compiledType = compilationResult.CompiledType;
var newExpression = Expression.New(compiledType);
var pathProperty = compiledType.GetProperty(nameof(IRazorPage.Path));
var propertyBindExpression = Expression.Bind(pathProperty, Expression.Constant(relativePath));
var objectInitializeExpression = Expression.MemberInit(newExpression, propertyBindExpression);
PageFactory = Expression
.Lambda<Func<IRazorPage>>(objectInitializeExpression)
.Compile();
IsPrecompiled = false;
}
@ -74,9 +52,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// Initializes a new instance of <see cref="CompilerCacheResult"/> for a file that could not be
/// found in the file system.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances that indicate when
/// this result has expired.</param>
public CompilerCacheResult(IList<IChangeToken> expirationTokens)
public CompilerCacheResult(string relativePath, IList<IChangeToken> expirationTokens)
{
if (expirationTokens == null)
{
@ -84,7 +63,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
ExpirationTokens = expirationTokens;
PageFactory = null;
RelativePath = null;
CompiledType = null;
IsPrecompiled = false;
}
@ -96,12 +76,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// <summary>
/// Gets a value that determines if the view was successfully found and compiled.
/// </summary>
public bool Success => PageFactory != null;
public bool Success => CompiledType != null;
/// <summary>
/// Gets a delegate that creates an instance of the <see cref="IRazorPage"/>.
/// Normalized relative path of the file.
/// </summary>
public Func<IRazorPage> PageFactory { get; }
public string RelativePath { get; }
/// <summary>
/// The compiled <see cref="Type"/>.
/// </summary>
public Type CompiledType { get; }
/// <summary>
/// Gets a value that determines if the view is precompiled.

View File

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
@ -13,37 +15,26 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// </summary>
public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider
{
/// <remarks>
/// This delegate holds on to an instance of <see cref="IRazorCompilationService"/>.
/// </remarks>
private readonly Func<RelativeFileInfo, CompilationResult> _compileDelegate;
private readonly ICompilerCacheProvider _compilerCacheProvider;
private ICompilerCache _compilerCache;
private const string ViewImportsFileName = "_ViewImports.cshtml";
private readonly RazorCompiler _razorCompiler;
/// <summary>
/// Initializes a new instance of <see cref="DefaultRazorPageFactoryProvider"/>.
/// </summary>
/// <param name="razorCompilationService">The <see cref="IRazorCompilationService"/>.</param>
/// <param name="razorEngine">The <see cref="RazorEngine"/>.</param>
/// <param name="razorProject">The <see cref="RazorProject" />.</param>
/// <param name="compilationService">The <see cref="ICompilationService"/>.</param>
/// <param name="compilerCacheProvider">The <see cref="ICompilerCacheProvider"/>.</param>
public DefaultRazorPageFactoryProvider(
IRazorCompilationService razorCompilationService,
RazorEngine razorEngine,
RazorProject razorProject,
ICompilationService compilationService,
ICompilerCacheProvider compilerCacheProvider)
{
_compileDelegate = razorCompilationService.Compile;
_compilerCacheProvider = compilerCacheProvider;
}
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
templateEngine.Options.ImportsFileName = ViewImportsFileName;
private ICompilerCache CompilerCache
{
get
{
if (_compilerCache == null)
{
_compilerCache = _compilerCacheProvider.Cache;
}
return _compilerCache;
}
_razorCompiler = new RazorCompiler(compilationService, compilerCacheProvider, templateEngine);
}
/// <inheritdoc />
@ -59,10 +50,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// For tilde slash paths, drop the leading ~ to make it work with the underlying IFileProvider.
relativePath = relativePath.Substring(1);
}
var result = CompilerCache.GetOrAdd(relativePath, _compileDelegate);
var result = _razorCompiler.Compile(relativePath);
if (result.Success)
{
return new RazorPageFactoryResult(result.PageFactory, result.ExpirationTokens, result.IsPrecompiled);
var compiledType = result.CompiledType;
var newExpression = Expression.New(compiledType);
var pathProperty = compiledType.GetTypeInfo().GetProperty(nameof(IRazorPage.Path));
// Generate: page.Path = relativePath;
// Use the normalized path specified from the result.
var propertyBindExpression = Expression.Bind(pathProperty, Expression.Constant(result.RelativePath));
var objectInitializeExpression = Expression.MemberInit(newExpression, propertyBindExpression);
var pageFactory = Expression
.Lambda<Func<IRazorPage>>(objectInitializeExpression)
.Compile();
return new RazorPageFactoryResult(pageFactory, result.ExpirationTokens, result.IsPrecompiled);
}
else
{

View File

@ -14,7 +14,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
private const string RazorFileExtension = ".cshtml";
private readonly IFileProvider _provider;
public DefaultRazorProject(IFileProvider provider)
public DefaultRazorProject(IRazorViewEngineFileProviderAccessor accessor)
: this(accessor.FileProvider)
{
}
// Internal for unit testing
internal DefaultRazorProject(IFileProvider provider)
{
_provider = provider;
}

View File

@ -20,6 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// <returns>A cached <see cref="CompilationResult"/>.</returns>
CompilerCacheResult GetOrAdd(
string relativePath,
Func<RelativeFileInfo, CompilationResult> compile);
Func<string, CompilerCacheContext> compile);
}
}

View File

@ -1,203 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
/// <summary>
/// Default implementation of <see cref="IRazorCompilationService"/>.
/// </summary>
public class RazorCompilationService : IRazorCompilationService
{
private readonly ICompilationService _compilationService;
private readonly RazorEngine _engine;
private readonly RazorProject _project;
private readonly IFileProvider _fileProvider;
private readonly ILogger _logger;
/// <summary>
/// Instantiates a new instance of the <see cref="RazorCompilationService"/> class.
/// </summary>
/// <param name="compilationService">The <see cref="ICompilationService"/> to compile generated code.</param>
/// <param name="engine">The <see cref="RazorEngine"/> to generate code from Razor files.</param>
/// <param name="project">The <see cref="RazorProject"/> implementation for locating files.</param>
/// <param name="fileProviderAccessor">The <see cref="IRazorViewEngineFileProviderAccessor"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public RazorCompilationService(
ICompilationService compilationService,
RazorEngine engine,
RazorProject project,
IRazorViewEngineFileProviderAccessor fileProviderAccessor,
ILoggerFactory loggerFactory)
{
_compilationService = compilationService;
_engine = engine;
_fileProvider = fileProviderAccessor.FileProvider;
_logger = loggerFactory.CreateLogger<RazorCompilationService>();
_project = project;
var stream = new MemoryStream();
var writer = new StreamWriter(stream, Encoding.UTF8);
writer.WriteLine("@using System");
writer.WriteLine("@using System.Linq");
writer.WriteLine("@using System.Collections.Generic");
writer.WriteLine("@using Microsoft.AspNetCore.Mvc");
writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering");
writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures");
writer.WriteLine("@inject Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel> Html");
writer.WriteLine("@inject Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json");
writer.WriteLine("@inject Microsoft.AspNetCore.Mvc.IViewComponentHelper Component");
writer.WriteLine("@inject Microsoft.AspNetCore.Mvc.IUrlHelper Url");
writer.WriteLine("@inject Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider");
writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor");
writer.Flush();
stream.Seek(0L, SeekOrigin.Begin);
GlobalImports = RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8);
}
public RazorSourceDocument GlobalImports { get; }
/// <inheritdoc />
public CompilationResult Compile(RelativeFileInfo file)
{
if (file == null)
{
throw new ArgumentNullException(nameof(file));
}
RazorCodeDocument codeDocument;
RazorCSharpDocument cSharpDocument;
using (var inputStream = file.FileInfo.CreateReadStream())
{
_logger.RazorFileToCodeCompilationStart(file.RelativePath);
var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0;
codeDocument = CreateCodeDocument(file.RelativePath, inputStream);
cSharpDocument = ProcessCodeDocument(codeDocument);
_logger.RazorFileToCodeCompilationEnd(file.RelativePath, startTimestamp);
}
if (cSharpDocument.Diagnostics.Count > 0)
{
return GetCompilationFailedResult(file.RelativePath, cSharpDocument.Diagnostics);
}
return _compilationService.Compile(codeDocument, cSharpDocument);
}
public virtual RazorCodeDocument CreateCodeDocument(string relativePath, Stream inputStream)
{
var absolutePath = _fileProvider.GetFileInfo(relativePath)?.PhysicalPath ?? relativePath;
var source = RazorSourceDocument.ReadFrom(inputStream, absolutePath);
var imports = new List<RazorSourceDocument>()
{
GlobalImports,
};
var paths = ViewHierarchyUtility.GetViewImportsLocations(relativePath);
foreach (var path in paths.Reverse())
{
var file = _fileProvider.GetFileInfo(path);
if (file.Exists)
{
using (var stream = file.CreateReadStream())
{
imports.Add(RazorSourceDocument.ReadFrom(stream, file.PhysicalPath ?? path));
}
}
}
var codeDocument = RazorCodeDocument.Create(source, imports);
codeDocument.SetRelativePath(relativePath);
return codeDocument;
}
public virtual RazorCSharpDocument ProcessCodeDocument(RazorCodeDocument codeDocument)
{
_engine.Process(codeDocument);
return codeDocument.GetCSharpDocument();
}
// Internal for unit testing
public CompilationResult GetCompilationFailedResult(
string relativePath,
IEnumerable<RazorDiagnostic> errors)
{
// If a SourceLocation does not specify a file path, assume it is produced
// from parsing the current file.
var messageGroups = errors
.GroupBy(razorError =>
razorError.Span.FilePath ?? relativePath,
StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
foreach (var group in messageGroups)
{
var filePath = group.Key;
var fileContent = ReadFileContentsSafely(filePath);
var compilationFailure = new CompilationFailure(
filePath,
fileContent,
compiledContent: string.Empty,
messages: group.Select(parserError => CreateDiagnosticMessage(parserError, filePath)));
failures.Add(compilationFailure);
}
return new CompilationResult(failures);
}
private DiagnosticMessage CreateDiagnosticMessage(
RazorDiagnostic error,
string filePath)
{
var sourceSpan = error.Span;
return new DiagnosticMessage(
message: error.GetMessage(),
formattedMessage: $"{error} ({sourceSpan.LineIndex},{sourceSpan.CharacterIndex}) {error.GetMessage()}",
filePath: filePath,
startLine: sourceSpan.LineIndex + 1,
startColumn: sourceSpan.CharacterIndex,
endLine: sourceSpan.LineIndex + 1,
endColumn: sourceSpan.CharacterIndex + sourceSpan.Length);
}
private string ReadFileContentsSafely(string relativePath)
{
var fileInfo = _fileProvider.GetFileInfo(relativePath);
if (fileInfo.Exists)
{
try
{
using (var reader = new StreamReader(fileInfo.CreateReadStream()))
{
return reader.ReadToEnd();
}
}
catch
{
// Ignore any failures
}
}
return null;
}
}
}

View File

@ -0,0 +1,132 @@

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public class RazorCompiler
{
private readonly ICompilationService _compilationService;
private readonly ICompilerCacheProvider _compilerCacheProvider;
private readonly MvcRazorTemplateEngine _templateEngine;
private readonly Func<string, CompilerCacheContext> _getCacheContext;
private readonly Func<CompilerCacheContext, CompilationResult> _getCompilationResultDelegate;
public RazorCompiler(
ICompilationService compilationService,
ICompilerCacheProvider compilerCacheProvider,
MvcRazorTemplateEngine templateEngine)
{
_compilationService = compilationService;
_compilerCacheProvider = compilerCacheProvider;
_templateEngine = templateEngine;
_getCacheContext = GetCacheContext;
_getCompilationResultDelegate = GetCompilationResult;
}
private ICompilerCache CompilerCache => _compilerCacheProvider.Cache;
public CompilerCacheResult Compile(string relativePath)
{
return CompilerCache.GetOrAdd(relativePath, _getCacheContext);
}
private CompilerCacheContext GetCacheContext(string path)
{
var item = _templateEngine.Project.GetItem(path);
var imports = _templateEngine.Project.FindHierarchicalItems(path, _templateEngine.Options.ImportsFileName);
return new CompilerCacheContext(item, imports, GetCompilationResult);
}
private CompilationResult GetCompilationResult(CompilerCacheContext cacheContext)
{
var projectItem = cacheContext.ProjectItem;
var codeDocument = _templateEngine.CreateCodeDocument(projectItem.Path);
var cSharpDocument = _templateEngine.GenerateCode(codeDocument);
CompilationResult compilationResult;
if (cSharpDocument.Diagnostics.Count > 0)
{
compilationResult = GetCompilationFailedResult(
codeDocument,
cSharpDocument.Diagnostics);
}
else
{
compilationResult = _compilationService.Compile(codeDocument, cSharpDocument);
}
return compilationResult;
}
internal CompilationResult GetCompilationFailedResult(
RazorCodeDocument codeDocument,
IEnumerable<RazorDiagnostic> diagnostics)
{
// If a SourceLocation does not specify a file path, assume it is produced from parsing the current file.
var messageGroups = diagnostics.GroupBy(
razorError => razorError.Span.FilePath ?? codeDocument.Source.FileName,
StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
foreach (var group in messageGroups)
{
var filePath = group.Key;
var fileContent = ReadContent(codeDocument, filePath);
var compilationFailure = new CompilationFailure(
filePath,
fileContent,
compiledContent: string.Empty,
messages: group.Select(parserError => CreateDiagnosticMessage(parserError, filePath)));
failures.Add(compilationFailure);
}
return new CompilationResult(failures);
}
private static string ReadContent(RazorCodeDocument codeDocument, string filePath)
{
RazorSourceDocument sourceDocument = null;
if (string.IsNullOrEmpty(filePath) || string.Equals(codeDocument.Source.FileName, filePath, StringComparison.Ordinal))
{
sourceDocument = codeDocument.Source;
}
else
{
sourceDocument = codeDocument.Imports.FirstOrDefault(f => string.Equals(f.FileName, filePath, StringComparison.Ordinal));
}
if (sourceDocument != null)
{
var contentChars = new char[sourceDocument.Length];
sourceDocument.CopyTo(0, contentChars, 0, sourceDocument.Length);
return new string(contentChars);
}
return string.Empty;
}
private static DiagnosticMessage CreateDiagnosticMessage(
RazorDiagnostic razorDiagnostic,
string filePath)
{
var sourceSpan = razorDiagnostic.Span;
var message = razorDiagnostic.GetMessage();
return new DiagnosticMessage(
message: message,
formattedMessage: razorDiagnostic.ToString(),
filePath: filePath,
startLine: sourceSpan.LineIndex + 1,
startColumn: sourceSpan.CharacterIndex,
endLine: sourceSpan.LineIndex + 1,
endColumn: sourceSpan.CharacterIndex + sourceSpan.Length);
}
}
}

View File

@ -58,22 +58,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("FlushPointCannotBeInvoked"), p0);
}
/// <summary>
/// The {0} returned by '{1}' must be an instance of '{2}'.
/// </summary>
internal static string Instrumentation_WriterMustBeBufferedTextWriter
{
get { return GetString("Instrumentation_WriterMustBeBufferedTextWriter"); }
}
/// <summary>
/// The {0} returned by '{1}' must be an instance of '{2}'.
/// </summary>
internal static string FormatInstrumentation_WriterMustBeBufferedTextWriter(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Instrumentation_WriterMustBeBufferedTextWriter"), p0, p1, p2);
}
/// <summary>
/// The layout view '{0}' could not be located. The following locations were searched:{1}
/// </summary>
@ -314,70 +298,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1);
}
/// <summary>
/// '{0}' must be a {1} that is generated as result of the call to '{2}'.
/// </summary>
internal static string ViewLocationCache_KeyMustBeString
{
get { return GetString("ViewLocationCache_KeyMustBeString"); }
}
/// <summary>
/// '{0}' must be a {1} that is generated as result of the call to '{2}'.
/// </summary>
internal static string FormatViewLocationCache_KeyMustBeString(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationCache_KeyMustBeString"), p0, p1, p2);
}
/// <summary>
/// The '{0}' method must be called before '{1}' can be invoked.
/// </summary>
internal static string ViewMustBeContextualized
{
get { return GetString("ViewMustBeContextualized"); }
}
/// <summary>
/// The '{0}' method must be called before '{1}' can be invoked.
/// </summary>
internal static string FormatViewMustBeContextualized(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewMustBeContextualized"), p0, p1);
}
/// <summary>
/// Unsupported hash algorithm.
/// </summary>
internal static string RazorHash_UnsupportedHashAlgorithm
{
get { return GetString("RazorHash_UnsupportedHashAlgorithm"); }
}
/// <summary>
/// Unsupported hash algorithm.
/// </summary>
internal static string FormatRazorHash_UnsupportedHashAlgorithm()
{
return GetString("RazorHash_UnsupportedHashAlgorithm");
}
/// <summary>
/// The resource '{0}' specified by '{1}' could not be found.
/// </summary>
internal static string RazorFileInfoCollection_ResourceCouldNotBeFound
{
get { return GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"); }
}
/// <summary>
/// The resource '{0}' specified by '{1}' could not be found.
/// </summary>
internal static string FormatRazorFileInfoCollection_ResourceCouldNotBeFound(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"), p0, p1);
}
/// <summary>
/// Generated Code
/// </summary>
@ -526,6 +446,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return GetString("RazorProject_PathMustStartWithForwardSlash");
}
/// <summary>
/// The property '{0}' of '{1}' must not be null.
/// </summary>
internal static string PropertyMustBeSet
{
get { return GetString("PropertyMustBeSet"); }
}
/// <summary>
/// The property '{0}' of '{1}' must not be null.
/// </summary>
internal static string FormatPropertyMustBeSet(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyMustBeSet"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -11,6 +11,7 @@ using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -30,6 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
public class RazorViewEngine : IRazorViewEngine
{
public static readonly string ViewExtension = ".cshtml";
private const string ViewStartFileName = "_ViewStart.cshtml";
private const string ControllerKey = "controller";
private const string AreaKey = "area";
@ -42,6 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
private readonly HtmlEncoder _htmlEncoder;
private readonly ILogger _logger;
private readonly RazorViewEngineOptions _options;
private readonly RazorProject _razorProject;
/// <summary>
/// Initializes a new instance of the <see cref="RazorViewEngine" />.
@ -51,6 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor,
RazorProject razorProject,
ILoggerFactory loggerFactory)
{
_options = optionsAccessor.Value;
@ -73,6 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
_pageActivator = pageActivator;
_htmlEncoder = htmlEncoder;
_logger = loggerFactory.CreateLogger<RazorViewEngine>();
_razorProject = razorProject;
ViewLookupCache = new MemoryCache(new MemoryCacheOptions
{
CompactOnMemoryPressure = false
@ -483,10 +488,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
string path,
HashSet<IChangeToken> expirationTokens)
{
var applicationRelativePath = MakePathApplicationRelative(path);
var viewStartPages = new List<ViewLocationCacheItem>();
foreach (var viewStartPath in ViewHierarchyUtility.GetViewStartLocations(path))
foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(applicationRelativePath, ViewStartFileName))
{
var result = _pageFactory.CreateFactory(viewStartPath);
var result = _pageFactory.CreateFactory(viewStartProjectItem.Path);
if (result.ExpirationTokens != null)
{
for (var i = 0; i < result.ExpirationTokens.Count; i++)
@ -500,7 +507,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Populate the viewStartPages list so that _ViewStarts appear in the order the need to be
// executed (closest last, furthest first). This is the reverse order in which
// ViewHierarchyUtility.GetViewStartLocations returns _ViewStarts.
viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, viewStartPath));
viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, viewStartProjectItem.Path));
}
}
@ -533,6 +540,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return name[0] == '~' || name[0] == '/';
}
private string MakePathApplicationRelative(string path)
{
Debug.Assert(!string.IsNullOrEmpty(path));
if (path[0] == '~')
{
path = path.Substring(1);
}
if (path[0] != '/')
{
path = '/' + path;
}
return path;
}
private static bool IsRelativePath(string name)
{
Debug.Assert(!string.IsNullOrEmpty(name));

View File

@ -126,9 +126,6 @@
<data name="FlushPointCannotBeInvoked" xml:space="preserve">
<value>'{0}' cannot be invoked when a Layout page is set to be executed.</value>
</data>
<data name="Instrumentation_WriterMustBeBufferedTextWriter" xml:space="preserve">
<value>The {0} returned by '{1}' must be an instance of '{2}'.</value>
</data>
<data name="LayoutCannotBeLocated" xml:space="preserve">
<value>The layout view '{0}' could not be located. The following locations were searched:{1}</value>
</data>
@ -174,18 +171,6 @@
<data name="ViewContextMustBeSet" xml:space="preserve">
<value>'{0} must be set to access '{1}'.</value>
</data>
<data name="ViewLocationCache_KeyMustBeString" xml:space="preserve">
<value>'{0}' must be a {1} that is generated as result of the call to '{2}'.</value>
</data>
<data name="ViewMustBeContextualized" xml:space="preserve">
<value>The '{0}' method must be called before '{1}' can be invoked.</value>
</data>
<data name="RazorHash_UnsupportedHashAlgorithm" xml:space="preserve">
<value>Unsupported hash algorithm.</value>
</data>
<data name="RazorFileInfoCollection_ResourceCouldNotBeFound" xml:space="preserve">
<value>The resource '{0}' specified by '{1}' could not be found.</value>
</data>
<data name="GeneratedCodeFileName" xml:space="preserve">
<value>Generated Code</value>
</data>
@ -215,4 +200,7 @@
<data name="RazorProject_PathMustStartWithForwardSlash" xml:space="preserve">
<value>Path must begin with a forward slash '/'.</value>
</data>
<data name="PropertyMustBeSet" xml:space="preserve">
<value>The property '{0}' of '{1}' must not be null.</value>
</data>
</root>

View File

@ -1,9 +1,18 @@
using System;
// 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.
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
/// <summary>
/// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>.
/// </summary>
public interface IPageLoader
{
/// <summary>
/// Produces a <see cref="CompiledPageActionDescriptor"/> given a <see cref="PageActionDescriptor"/>.
/// </summary>
/// <param name="actionDescriptor">The <see cref="PageActionDescriptor"/>.</param>
/// <returns>The <see cref="CompiledPageActionDescriptor"/>.</returns>
CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor);
}
}

View File

@ -1,123 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultPageLoader : IPageLoader
{
private const string PageImportsFileName = "_PageImports.cshtml";
private const string ModelPropertyName = "Model";
private readonly RazorCompilationService _razorCompilationService;
private readonly ICompilationService _compilationService;
private readonly RazorProject _project;
private readonly ILogger _logger;
private readonly MvcRazorTemplateEngine _templateEngine;
private readonly RazorCompiler _razorCompiler;
public DefaultPageLoader(
IRazorCompilationService razorCompilationService,
ICompilationService compilationService,
RazorEngine razorEngine,
RazorProject razorProject,
ILogger<DefaultPageLoader> logger)
ICompilationService compilationService,
ICompilerCacheProvider compilerCacheProvider)
{
_razorCompilationService = (RazorCompilationService)razorCompilationService;
_compilationService = compilationService;
_project = razorProject;
_logger = logger;
_templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
_templateEngine.Options.ImportsFileName = PageImportsFileName;
_razorCompiler = new RazorCompiler(compilationService, compilerCacheProvider, _templateEngine);
}
public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor)
{
var item = _project.GetItem(actionDescriptor.RelativePath);
if (!item.Exists)
{
throw new InvalidOperationException($"File {actionDescriptor.RelativePath} was not found.");
}
_logger.RazorFileToCodeCompilationStart(item.Path);
var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0;
var codeDocument = CreateCodeDocument(item);
var cSharpDocument = _razorCompilationService.ProcessCodeDocument(codeDocument);
_logger.RazorFileToCodeCompilationEnd(item.Path, startTimestamp);
CompilationResult compilationResult;
if (cSharpDocument.Diagnostics.Count > 0)
{
compilationResult = _razorCompilationService.GetCompilationFailedResult(item.Path, cSharpDocument.Diagnostics);
}
else
{
compilationResult = _compilationService.Compile(codeDocument, cSharpDocument);
}
compilationResult.EnsureSuccessful();
var compilationResult = _razorCompiler.Compile(actionDescriptor.RelativePath);
var compiledTypeInfo = compilationResult.CompiledType.GetTypeInfo();
// If a model type wasn't set in code then the model property's type will be the same
// as the compiled type.
var pageType = compilationResult.CompiledType.GetTypeInfo();
var modelType = pageType.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo();
if (modelType == pageType)
var modelTypeInfo = compiledTypeInfo.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo();
if (modelTypeInfo == compiledTypeInfo)
{
modelType = null;
modelTypeInfo = null;
}
return new CompiledPageActionDescriptor(actionDescriptor)
{
ModelTypeInfo = modelType,
PageTypeInfo = pageType,
PageTypeInfo = compiledTypeInfo,
ModelTypeInfo = modelTypeInfo,
};
}
private RazorCodeDocument CreateCodeDocument(RazorProjectItem item)
{
var absolutePath = GetItemPath(item);
RazorSourceDocument source;
using (var inputStream = item.Read())
{
source = RazorSourceDocument.ReadFrom(inputStream, absolutePath);
}
var imports = new List<RazorSourceDocument>()
{
_razorCompilationService.GlobalImports,
};
var pageImports = _project.FindHierarchicalItems(item.Path, "_PageImports.cshtml");
foreach (var pageImport in pageImports.Reverse())
{
if (pageImport.Exists)
{
using (var stream = pageImport.Read())
{
imports.Add(RazorSourceDocument.ReadFrom(stream, GetItemPath(item)));
}
}
}
return RazorCodeDocument.Create(source, imports);
}
private static string GetItemPath(RazorProjectItem item)
{
var absolutePath = item.Path;
if (item.Exists && string.IsNullOrEmpty(item.PhysicalPath))
{
absolutePath = item.PhysicalPath;
}
return absolutePath;
}
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.FileProviders;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Host
{
public class MvcRazorTemplateEngineTest
{
[Fact]
public void GetDefaultImports_IncludesDefaultImports()
{
// Arrange
var expectedImports = new[]
{
"@using System",
"@using System.Linq",
"@using System.Collections.Generic",
"@using Microsoft.AspNetCore.Mvc",
"@using Microsoft.AspNetCore.Mvc.Rendering",
"@using Microsoft.AspNetCore.Mvc.ViewFeatures",
};
var mvcRazorTemplateEngine = new MvcRazorTemplateEngine(
RazorEngine.Create(),
GetRazorProject(new TestFileProvider()));
// Act
var imports = mvcRazorTemplateEngine.Options.DefaultImports;
// Assert
var importContent = GetContent(imports)
.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Where(line => line.StartsWith("@using"));
Assert.Equal(expectedImports, importContent);
}
[Fact]
public void GetDefaultImports_IncludesDefaulInjects()
{
// Arrange
var expectedImports = new[]
{
"@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel> Html",
"@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json",
"@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component",
"@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url",
"@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider",
};
var mvcRazorTemplateEngine = new MvcRazorTemplateEngine(
RazorEngine.Create(),
GetRazorProject(new TestFileProvider()));
// Act
var imports = mvcRazorTemplateEngine.Options.DefaultImports;
// Assert
var importContent = GetContent(imports)
.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Where(line => line.StartsWith("@inject"));
Assert.Equal(expectedImports, importContent);
}
[Fact]
public void GetDefaultImports_IncludesUrlTagHelper()
{
// Arrange
var mvcRazorTemplateEngine = new MvcRazorTemplateEngine(
RazorEngine.Create(),
GetRazorProject(new TestFileProvider()));
// Act
var imports = mvcRazorTemplateEngine.Options.DefaultImports;
// Assert
var importContent = GetContent(imports)
.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Where(line => line.StartsWith("@addTagHelper"));
var addTagHelper = Assert.Single(importContent);
Assert.Equal("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor",
addTagHelper);
}
[Fact]
public void CreateCodeDocument_SetsRelativePathOnOutput()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
fileProvider.AddFile(path, "Hello world");
var mvcRazorTemplateEngine = new MvcRazorTemplateEngine(
RazorEngine.Create(),
GetRazorProject(fileProvider));
// Act
var codeDocument = mvcRazorTemplateEngine.CreateCodeDocument(path);
// Assert
Assert.Equal(path, codeDocument.GetRelativePath());
}
private string GetContent(RazorSourceDocument imports)
{
var contentChars = new char[imports.Length];
imports.CopyTo(0, contentChars, 0, imports.Length);
return new string(contentChars);
}
private static DefaultRazorProject GetRazorProject(IFileProvider fileProvider)
{
var fileProviderAccessor = new Mock<IRazorViewEngineFileProviderAccessor>();
fileProviderAccessor.SetupGet(f => f.FileProvider)
.Returns(fileProvider);
return new DefaultRazorProject(fileProviderAccessor.Object);
}
}
}

View File

@ -1,277 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor
{
public class ViewHierarchyUtilityTest
{
[Theory]
[InlineData(null)]
[InlineData("")]
public void GetViewStartLocations_ReturnsEmptySequenceIfViewPathIsEmpty(string viewPath)
{
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(viewPath);
// Assert
Assert.Empty(result);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void GetViewImportsLocations_ReturnsEmptySequenceIfViewPathIsEmpty(string viewPath)
{
// Act
var result = ViewHierarchyUtility.GetViewImportsLocations(viewPath);
// Assert
Assert.Empty(result);
}
[Theory]
[InlineData("/Views/Home/MyView.cshtml")]
[InlineData("~/Views/Home/MyView.cshtml")]
[InlineData("Views/Home/MyView.cshtml")]
public void GetViewStartLocations_ReturnsPotentialViewStartLocations_PathStartswithSlash(string inputPath)
{
// Arrange
var expected = new[]
{
"/Views/Home/_ViewStart.cshtml",
"/Views/_ViewStart.cshtml",
"/_ViewStart.cshtml"
};
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(inputPath);
// Assert
Assert.Equal(expected, result);
}
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux,
SkipReason = "Back slashes only work as path separators on Windows")]
[OSSkipCondition(OperatingSystems.MacOSX,
SkipReason = "Back slashes only work as path separators on Windows")]
[InlineData(@"~/Views\Home\MyView.cshtml")]
[InlineData(@"Views\Home\MyView.cshtml")]
public void GetViewStartLocations_ReturnsPotentialViewStartLocations_PathsContainBackSlash(
string inputPath)
{
// Arrange
var expected = new[]
{
"/Views/Home/_ViewStart.cshtml",
"/Views/_ViewStart.cshtml",
"/_ViewStart.cshtml"
};
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(inputPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("/Views/Home/MyView.cshtml")]
[InlineData("~/Views/Home/MyView.cshtml")]
[InlineData("Views/Home/MyView.cshtml")]
public void GetViewImportsLocations_ReturnsPotentialViewStartLocations_PathStartswithSlash(string inputPath)
{
// Arrange
var expected = new[]
{
"/Views/Home/_ViewImports.cshtml",
"/Views/_ViewImports.cshtml",
"/_ViewImports.cshtml"
};
// Act
var result = ViewHierarchyUtility.GetViewImportsLocations(inputPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("/Views/Home/_ViewStart.cshtml")]
[InlineData("~/Views/Home/_ViewStart.cshtml")]
[InlineData("Views/Home/_ViewStart.cshtml")]
public void GetViewStartLocations_SkipsCurrentPath_IfCurrentIsViewStart(string inputPath)
{
// Arrange
var expected = new[]
{
"/Views/_ViewStart.cshtml",
"/_ViewStart.cshtml"
};
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(inputPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("/Views/Home/_ViewStart.cshtml")]
[InlineData("~/Views/Home/_ViewStart.cshtml")]
[InlineData("Views/Home/_ViewStart.cshtml")]
public void GetViewImportsLocations_WhenCurrentIsViewStart(string inputPath)
{
// Arrange
var expected = new[]
{
"/Views/Home/_ViewImports.cshtml",
"/Views/_ViewImports.cshtml",
"/_ViewImports.cshtml"
};
// Act
var result = ViewHierarchyUtility.GetViewImportsLocations(inputPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("/Views/Home/_ViewImports.cshtml")]
[InlineData("~/Views/Home/_ViewImports.cshtml")]
[InlineData("Views/Home/_ViewImports.cshtml")]
public void GetViewImportsLocations_SkipsCurrentPath_IfCurrentIsViewImports(string inputPath)
{
// Arrange
var expected = new[]
{
"/Views/_ViewImports.cshtml",
"/_ViewImports.cshtml"
};
// Act
var result = ViewHierarchyUtility.GetViewImportsLocations(inputPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("Test.cshtml")]
[InlineData("ViewStart.cshtml")]
public void GetViewStartLocations_ReturnsPotentialViewStartLocations(string fileName)
{
// Arrange
var expected = new[]
{
"/Areas/MyArea/Sub/Views/Admin/_ViewStart.cshtml",
"/Areas/MyArea/Sub/Views/_ViewStart.cshtml",
"/Areas/MyArea/Sub/_ViewStart.cshtml",
"/Areas/MyArea/_ViewStart.cshtml",
"/Areas/_ViewStart.cshtml",
"/_ViewStart.cshtml",
};
var viewPath = $"Areas/MyArea/Sub/Views/Admin/{fileName}";
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(viewPath);
// Assert
Assert.Equal(expected, result);
}
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux,
SkipReason = "Back slashes only work as path separators on Windows")]
[OSSkipCondition(OperatingSystems.MacOSX,
SkipReason = "Back slashes only work as path separators on Windows")]
[InlineData("Test.cshtml")]
[InlineData("ViewStart.cshtml")]
public void GetViewStartLocations_ReturnsPotentialViewStartLocations_ForPathsWithBackSlashes(string fileName)
{
// Arrange
var expected = new[]
{
"/Areas/MyArea/Sub/Views/Admin/_ViewStart.cshtml",
"/Areas/MyArea/Sub/Views/_ViewStart.cshtml",
"/Areas/MyArea/Sub/_ViewStart.cshtml",
"/Areas/MyArea/_ViewStart.cshtml",
"/Areas/_ViewStart.cshtml",
"/_ViewStart.cshtml",
};
var viewPath = $"Areas\\MyArea\\Sub\\Views\\Admin/{fileName}";
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(viewPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("Test.cshtml")]
[InlineData("Global.cshtml")]
[InlineData("_ViewStart.cshtml")]
public void GetViewImportsLocations_ReturnsPotentialGlobalLocations(string fileName)
{
// Arrange
var expected = new[]
{
"/Areas/MyArea/Sub/Views/Admin/_ViewImports.cshtml",
"/Areas/MyArea/Sub/Views/_ViewImports.cshtml",
"/Areas/MyArea/Sub/_ViewImports.cshtml",
"/Areas/MyArea/_ViewImports.cshtml",
"/Areas/_ViewImports.cshtml",
"/_ViewImports.cshtml",
};
var viewPath = $"Areas/MyArea/Sub/Views/Admin/{fileName}";
// Act
var result = ViewHierarchyUtility.GetViewImportsLocations(viewPath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData("_ViewStart.cshtml")]
[InlineData("_viewstart.cshtml")]
public void GetViewStartLocations_SkipsCurrentPath_IfPathIsAViewStartFile(string fileName)
{
// Arrange
var expected = new[]
{
"/Areas/MyArea/Sub/Views/_ViewStart.cshtml",
"/Areas/MyArea/Sub/_ViewStart.cshtml",
"/Areas/MyArea/_ViewStart.cshtml",
"/Areas/_ViewStart.cshtml",
"/_ViewStart.cshtml",
};
var viewPath = $"Areas/MyArea/Sub/Views/Admin/{fileName}";
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(viewPath);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void GetViewStartLocations_ReturnsEmptySequence_IfViewStartIsAtRoot()
{
// Arrange
var viewPath = "_ViewStart.cshtml";
// Act
var result = ViewHierarchyUtility.GetViewStartLocations(viewPath);
// Assert
Assert.Empty(result);
}
}
}

View File

@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.FileProviders;
using Moq;
using Xunit;
@ -17,18 +19,30 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
private const string ViewPath = "/Views/Home/Index.cshtml";
private const string PrecompiledViewsPath = "/Views/Home/Precompiled.cshtml";
private static readonly string[] _viewImportsPath = new[]
{
"/Views/Home/_ViewImports.cshtml",
"/Views/_ViewImports.cshtml",
"/_ViewImports.cshtml",
};
private readonly IDictionary<string, Type> _precompiledViews = new Dictionary<string, Type>
{
{ PrecompiledViewsPath, typeof(PreCompile) }
};
public static TheoryData ViewImportsPaths =>
new TheoryData<string>
public static TheoryData ViewImportsPaths
{
get
{
"/Views/Home/_ViewImports.cshtml",
"/Views/_ViewImports.cshtml",
"/_ViewImports.cshtml",
};
var theoryData = new TheoryData<string>();
foreach (var path in _viewImportsPath)
{
theoryData.Add(path);
}
return theoryData;
}
}
[Fact]
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
@ -36,9 +50,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Arrange
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(fileProvider);
var compilerCacheContext = new CompilerCacheContext(
new NotFoundProjectItem("", "/path"),
Enumerable.Empty<RazorProjectItem>(),
_ => throw new Exception("Shouldn't be called."));
// Act
var result = cache.GetOrAdd("/some/path", ThrowsIfCalled);
var result = cache.GetOrAdd("/some/path", _ => compilerCacheContext);
// Assert
Assert.False(result.Success);
@ -54,12 +72,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var expected = new CompilationResult(typeof(TestView));
// Act
var result = cache.GetOrAdd(ViewPath, _ => expected);
var result = cache.GetOrAdd(ViewPath, CreateContextFactory(expected));
// Assert
Assert.True(result.Success);
Assert.IsType<TestView>(result.PageFactory());
Assert.Same(ViewPath, result.PageFactory().Path);
Assert.Equal(typeof(TestView), result.CompiledType);
Assert.Equal(ViewPath, result.RelativePath);
}
[Theory]
@ -77,17 +95,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var expected = new CompilationResult(typeof(TestView));
// Act - 1
var result1 = cache.GetOrAdd(@"Areas\Finances\Views\Home\Index.cshtml", _ => expected);
var result1 = cache.GetOrAdd(@"Areas\Finances\Views\Home\Index.cshtml", CreateContextFactory(expected));
// Assert - 1
Assert.IsType<TestView>(result1.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act - 2
var result2 = cache.GetOrAdd(relativePath, ThrowsIfCalled);
// Assert - 2
Assert.IsType<TestView>(result2.PageFactory());
Assert.Same(result1.PageFactory, result2.PageFactory);
Assert.Equal(typeof(TestView), result2.CompiledType);
}
[Fact]
@ -95,22 +112,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
// Arrange
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var fileInfo = fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider);
var expected = new CompilationResult(typeof(TestView));
var projectItem = new DefaultRazorProjectItem(fileInfo, "", ViewPath);
var cacheContext = new CompilerCacheContext(projectItem, Enumerable.Empty<RazorProjectItem>(), _ => expected);
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
var result1 = cache.GetOrAdd(ViewPath, _ => cacheContext);
// Assert 1
Assert.True(result1.Success);
Assert.IsType<TestView>(result1.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 2
// Delete the file from the file system and set it's expiration token.
fileProvider.DeleteFile(ViewPath);
cacheContext = new CompilerCacheContext(
new NotFoundProjectItem("", ViewPath),
Enumerable.Empty<RazorProjectItem>(),
_ => throw new Exception("Shouldn't be called."));
fileProvider.GetChangeToken(ViewPath).HasChanged = true;
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
var result2 = cache.GetOrAdd(ViewPath, _ => cacheContext);
// Assert 2
Assert.False(result2.Success);
@ -127,11 +149,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var expected2 = new CompilationResult(typeof(DifferentView));
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected1));
// Assert 1
Assert.True(result1.Success);
Assert.IsType<TestView>(result1.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 2
// Verify we're getting cached results.
@ -139,15 +161,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert 2
Assert.True(result2.Success);
Assert.IsType<TestView>(result2.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 3
fileProvider.GetChangeToken(ViewPath).HasChanged = true;
var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
var result3 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected2));
// Assert 3
Assert.True(result3.Success);
Assert.IsType<DifferentView>(result3.PageFactory());
Assert.Equal(typeof(DifferentView), result3.CompiledType);
}
[Theory]
@ -162,11 +184,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var expected2 = new CompilationResult(typeof(DifferentView));
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected1));
// Assert 1
Assert.True(result1.Success);
Assert.IsType<TestView>(result1.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 2
// Verify we're getting cached results.
@ -174,15 +196,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert 2
Assert.True(result2.Success);
Assert.IsType<TestView>(result2.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 3
fileProvider.GetChangeToken(globalImportPath).HasChanged = true;
var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
var result3 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected2));
// Assert 2
Assert.True(result3.Success);
Assert.IsType<DifferentView>(result3.PageFactory());
Assert.Equal(typeof(DifferentView), result3.CompiledType);
}
[Fact]
@ -196,19 +218,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var expected = new CompilationResult(typeof(TestView));
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected));
// Assert 1
Assert.True(result1.Success);
Assert.IsType<TestView>(result1.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 2
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.True(result2.Success);
Assert.IsType<TestView>(result2.PageFactory());
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
}
[Fact]
@ -223,8 +243,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
Assert.True(result.Success);
Assert.IsType<PreCompile>(result.PageFactory());
Assert.Same(PrecompiledViewsPath, result.PageFactory().Path);
Assert.Equal(typeof(PreCompile), result.CompiledType);
Assert.Same(PrecompiledViewsPath, result.RelativePath);
}
[Fact]
@ -242,7 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
Assert.True(result.Success);
Assert.True(result.IsPrecompiled);
Assert.IsType<PreCompile>(result.PageFactory());
Assert.Equal(typeof(PreCompile), result.CompiledType);
}
[Theory]
@ -260,11 +280,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
Assert.True(result.Success);
Assert.IsType<PreCompile>(result.PageFactory());
Assert.Equal(typeof(PreCompile), result.CompiledType);
}
[Fact]
public void GetOrAdd_ReturnsRuntimeCompiledAndPrecompiledViews()
public void GetOrAdd_ReturnsRuntimeCompiled()
{
// Arrange
var fileProvider = new TestFileProvider();
@ -273,24 +293,32 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var expected = new CompilationResult(typeof(TestView));
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected));
// Assert 1
Assert.IsType<TestView>(result1.PageFactory());
Assert.Equal(typeof(TestView), result1.CompiledType);
// Act 2
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.True(result2.Success);
Assert.IsType<TestView>(result2.PageFactory());
Assert.Equal(typeof(TestView), result2.CompiledType);
}
// Act 3
var result3 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
[Fact]
public void GetOrAdd_ReturnsPrecompiledViews()
{
// Arrange
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(fileProvider, _precompiledViews);
var expected = new CompilationResult(typeof(TestView));
// Assert 3
Assert.True(result2.Success);
Assert.IsType<PreCompile>(result3.PageFactory());
// Act
var result1 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert
Assert.Equal(typeof(PreCompile), result1.CompiledType);
}
[Theory]
@ -314,8 +342,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var result = cache.GetOrAdd(relativePath, ThrowsIfCalled);
// Assert
Assert.IsType<PreCompile>(result.PageFactory());
Assert.Same(viewPath, result.PageFactory().Path);
Assert.Equal(typeof(PreCompile), result.CompiledType);
Assert.Equal(viewPath, result.RelativePath);
}
[Theory]
@ -337,7 +365,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var result = cache.GetOrAdd("/Areas/Finances/Views/Home/Index.cshtml", ThrowsIfCalled);
// Assert
Assert.IsType<PreCompile>(result.PageFactory());
Assert.Equal(typeof(PreCompile), result.CompiledType);
}
[Fact]
@ -354,42 +382,55 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var compilingOne = false;
var compilingTwo = false;
Func<CompilerCacheContext, CompilationResult> compile1 = _ =>
{
compilingOne = true;
// Event 2
Assert.True(resetEvent1.WaitOne(waitDuration));
// Event 3
Assert.True(resetEvent2.Set());
// Event 6
Assert.True(resetEvent1.WaitOne(waitDuration));
Assert.True(compilingTwo);
return new CompilationResult(typeof(TestView));
};
Func<CompilerCacheContext, CompilationResult> compile2 = _ =>
{
compilingTwo = true;
// Event 4
Assert.True(resetEvent2.WaitOne(waitDuration));
// Event 5
Assert.True(resetEvent1.Set());
Assert.True(compilingOne);
return new CompilationResult(typeof(DifferentView));
};
// Act
var task1 = Task.Run(() =>
{
return cache.GetOrAdd("/Views/Home/Index.cshtml", file =>
return cache.GetOrAdd("/Views/Home/Index.cshtml", path =>
{
compilingOne = true;
// Event 2
resetEvent1.WaitOne(waitDuration);
// Event 3
resetEvent2.Set();
// Event 6
resetEvent1.WaitOne(waitDuration);
Assert.True(compilingTwo);
return new CompilationResult(typeof(TestView));
var projectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", path);
return new CompilerCacheContext(projectItem, Enumerable.Empty<RazorProjectItem>(), compile1);
});
});
var task2 = Task.Run(() =>
{
// Event 4
return cache.GetOrAdd("/Views/Home/About.cshtml", file =>
return cache.GetOrAdd("/Views/Home/About.cshtml", path =>
{
compilingTwo = true;
// Event 4
resetEvent2.WaitOne(waitDuration);
// Event 5
resetEvent1.Set();
Assert.True(compilingOne);
return new CompilationResult(typeof(DifferentView));
var projectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", path);
return new CompilerCacheContext(projectItem, Enumerable.Empty<RazorProjectItem>(), compile2);
});
});
@ -416,24 +457,30 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var resetEvent2 = new ManualResetEvent(initialState: false);
var cache = new CompilerCache(fileProvider);
Func<CompilerCacheContext, CompilationResult> compile = _ =>
{
// Event 2
resetEvent1.WaitOne(waitDuration);
// Event 3
resetEvent2.Set();
return new CompilationResult(typeof(TestView));
};
// Act
var task1 = Task.Run(() =>
{
return cache.GetOrAdd(ViewPath, file =>
return cache.GetOrAdd(ViewPath, path =>
{
// Event 2
resetEvent1.WaitOne(waitDuration);
// Event 3
resetEvent2.Set();
return new CompilationResult(typeof(TestView));
var projectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", path);
return new CompilerCacheContext(projectItem, Enumerable.Empty<RazorProjectItem>(), compile);
});
});
var task2 = Task.Run(() =>
{
// Event 4
resetEvent2.WaitOne(waitDuration);
Assert.True(resetEvent2.WaitOne(waitDuration));
return cache.GetOrAdd(ViewPath, ThrowsIfCalled);
});
@ -444,7 +491,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
var result1 = task1.Result;
var result2 = task2.Result;
Assert.Same(result1.PageFactory, result2.PageFactory);
Assert.Same(result1.CompiledType, result2.CompiledType);
}
[Fact]
@ -475,7 +522,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Act and Assert - 1
var actual = Assert.Throws<InvalidTimeZoneException>(() =>
cache.GetOrAdd(ViewPath, _ => { throw exception; }));
cache.GetOrAdd(ViewPath, _ => ThrowsIfCalled(ViewPath, exception)));
Assert.Same(exception, actual);
// Act and Assert - 2
@ -489,6 +536,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Arrange
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var changeToken = fileProvider.AddChangeToken(ViewPath);
var cache = new CompilerCache(fileProvider);
// Act and Assert - 1
@ -496,11 +544,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
cache.GetOrAdd(ViewPath, _ => { throw new InvalidTimeZoneException(); }));
// Act - 2
fileProvider.GetChangeToken(ViewPath).HasChanged = true;
var result = cache.GetOrAdd(ViewPath, _ => new CompilationResult(typeof(TestView)));
changeToken.HasChanged = true;
var result = cache.GetOrAdd(ViewPath, CreateContextFactory(new CompilationResult(typeof(TestView))));
// Assert - 2
Assert.IsType<TestView>(result.PageFactory());
Assert.Same(typeof(TestView), result.CompiledType);
}
[Fact]
@ -512,15 +560,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var cache = new CompilerCache(fileProvider);
var diagnosticMessages = new[]
{
new AspNetCore.Diagnostics.DiagnosticMessage("message", "message", ViewPath, 1, 1, 1, 1)
new DiagnosticMessage("message", "message", ViewPath, 1, 1, 1, 1)
};
var compilationResult = new CompilationResult(new[]
{
new CompilationFailure(ViewPath, "some content", "compiled content", diagnosticMessages)
});
var context = CreateContextFactory(compilationResult);
// Act and Assert - 1
var ex = Assert.Throws<CompilationFailedException>(() => cache.GetOrAdd(ViewPath, _ => compilationResult));
var ex = Assert.Throws<CompilationFailedException>(() => cache.GetOrAdd(ViewPath, context));
Assert.Same(compilationResult.CompilationFailures, ex.CompilationFailures);
// Act and Assert - 2
@ -552,9 +601,38 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
}
private CompilationResult ThrowsIfCalled(RelativeFileInfo file)
private CompilerCacheContext ThrowsIfCalled(string path) =>
ThrowsIfCalled(path, new Exception("Shouldn't be called"));
private CompilerCacheContext ThrowsIfCalled(string path, Exception exception)
{
throw new Exception("Shouldn't be called");
exception = exception ?? new Exception("Shouldn't be called");
var projectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", path);
return new CompilerCacheContext(
projectItem,
Enumerable.Empty<RazorProjectItem>(),
_ => throw exception);
}
private Func<string, CompilerCacheContext> CreateContextFactory(CompilationResult compile)
{
return path => CreateCacheContext(compile, path);
}
private CompilerCacheContext CreateCacheContext(CompilationResult compile, string path = ViewPath)
{
var projectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", path);
var imports = new List<RazorProjectItem>();
foreach (var importFilePath in _viewImportsPath)
{
var importProjectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", importFilePath);
imports.Add(importProjectItem);
}
return new CompilerCacheContext(projectItem, imports, _ => compile);
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForUnsuccessfulResults()
{
// Arrange
var path = "/file-does-not-exist";
var expirationTokens = new[]
{
Mock.Of<IChangeToken>(),
@ -23,18 +25,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
var compilerCache = new Mock<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<RelativeFileInfo, CompilationResult>>()))
.Returns(new CompilerCacheResult(expirationTokens));
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
.Returns(new CompilerCacheResult(path, expirationTokens));
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Returns(compilerCache.Object);
var factoryProvider = new DefaultRazorPageFactoryProvider(
Mock.Of<IRazorCompilationService>(),
RazorEngine.Create(),
new DefaultRazorProject(new TestFileProvider()),
Mock.Of<ICompilationService>(),
compilerCacheProvider.Object);
// Act
var result = factoryProvider.CreateFactory("/file-does-not-exist");
var result = factoryProvider.CreateFactory(path);
// Assert
Assert.False(result.Success);
@ -53,14 +57,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
var compilerCache = new Mock<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<RelativeFileInfo, CompilationResult>>()))
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
.Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), expirationTokens));
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Returns(compilerCache.Object);
var factoryProvider = new DefaultRazorPageFactoryProvider(
Mock.Of<IRazorCompilationService>(),
RazorEngine.Create(),
new DefaultRazorProject(new TestFileProvider()),
Mock.Of<ICompilationService>(),
compilerCacheProvider.Object);
// Act
@ -78,14 +84,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var relativePath = "/file-exists";
var compilerCache = new Mock<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<RelativeFileInfo, CompilationResult>>()))
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
.Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), new IChangeToken[0]));
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Returns(compilerCache.Object);
var factoryProvider = new DefaultRazorPageFactoryProvider(
Mock.Of<IRazorCompilationService>(),
RazorEngine.Create(),
new DefaultRazorProject(new TestFileProvider()),
Mock.Of<ICompilationService>(),
compilerCacheProvider.Object);
// Act

View File

@ -1,246 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public class RazorCompilationServiceTest
{
[Fact]
public void CompileCalculatesRootRelativePath()
{
// Arrange
var viewPath = @"src\work\myapp\Views\index\home.cshtml";
var relativePath = @"Views\index\home.cshtml";
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath);
fileInfo.Setup(f => f.CreateReadStream()).Returns(new MemoryStream(new byte[] { 0 }));
var fileProvider = new TestFileProvider();
fileProvider.AddFile(relativePath, fileInfo.Object);
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, relativePath);
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(It.IsAny<RazorCodeDocument>(), It.IsAny<RazorCSharpDocument>()))
.Returns(new CompilationResult(typeof(RazorCompilationServiceTest)));
var engine = new Mock<RazorEngine>();
engine.Setup(e => e.Process(It.IsAny<RazorCodeDocument>()))
.Callback<RazorCodeDocument>(document =>
{
document.SetCSharpDocument(new RazorCSharpDocument()
{
Diagnostics = new List<RazorDiagnostic>()
});
Assert.Equal(viewPath, document.Source.FileName); // Assert if source file name is the root relative path
}).Verifiable();
var razorService = new RazorCompilationService(
compiler.Object,
engine.Object,
new DefaultRazorProject(fileProvider),
GetFileProviderAccessor(fileProvider),
NullLoggerFactory.Instance);
// Act
razorService.Compile(relativeFileInfo);
// Assert
engine.Verify();
}
[Fact]
public void Compile_ReturnsFailedResultIfParseFails()
{
// Arrange
var relativePath = @"Views\index\home.cshtml";
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.CreateReadStream()).Returns(new MemoryStream(new byte[] { 0 }));
var fileProvider = new TestFileProvider();
fileProvider.AddFile(relativePath, fileInfo.Object);
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, relativePath);
var compiler = new Mock<ICompilationService>(MockBehavior.Strict);
var engine = new Mock<RazorEngine>();
engine.Setup(e => e.Process(It.IsAny<RazorCodeDocument>()))
.Callback<RazorCodeDocument>(document =>
{
document.SetCSharpDocument(new RazorCSharpDocument()
{
Diagnostics = new List<RazorDiagnostic>()
{
GetRazorDiagnostic("some message", new SourceLocation(1, 1, 1), length: 1)
}
});
}).Verifiable();
var razorService = new RazorCompilationService(
compiler.Object,
engine.Object,
new DefaultRazorProject(fileProvider),
GetFileProviderAccessor(fileProvider),
NullLoggerFactory.Instance);
// Act
var result = razorService.Compile(relativeFileInfo);
// Assert
Assert.NotNull(result.CompilationFailures);
Assert.Collection(result.CompilationFailures,
failure =>
{
var message = Assert.Single(failure.Messages);
Assert.Equal("some message", message.Message);
});
engine.Verify();
}
[Fact]
public void Compile_ReturnsResultFromCompilationServiceIfParseSucceeds()
{
var relativePath = @"Views\index\home.cshtml";
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.CreateReadStream()).Returns(new MemoryStream(new byte[] { 0 }));
var fileProvider = new TestFileProvider();
fileProvider.AddFile(relativePath, fileInfo.Object);
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, relativePath);
var compilationResult = new CompilationResult(typeof(object));
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(It.IsAny<RazorCodeDocument>(), It.IsAny<RazorCSharpDocument>()))
.Returns(compilationResult)
.Verifiable();
var engine = new Mock<RazorEngine>();
engine.Setup(e => e.Process(It.IsAny<RazorCodeDocument>()))
.Callback<RazorCodeDocument>(document =>
{
document.SetCSharpDocument(new RazorCSharpDocument()
{
Diagnostics = new List<RazorDiagnostic>()
});
});
var razorService = new RazorCompilationService(
compiler.Object,
engine.Object,
new DefaultRazorProject(fileProvider),
GetFileProviderAccessor(fileProvider),
NullLoggerFactory.Instance);
// Act
var result = razorService.Compile(relativeFileInfo);
// Assert
Assert.Same(compilationResult.CompiledType, result.CompiledType);
compiler.Verify();
}
[Fact]
public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages()
{
// Arrange
var viewPath = @"views/index.razor";
var viewImportsPath = @"views/global.import.cshtml";
var fileProvider = new TestFileProvider();
var file = fileProvider.AddFile(viewPath, "View Content");
fileProvider.AddFile(viewImportsPath, "Global Import Content");
var razorService = new RazorCompilationService(
Mock.Of<ICompilationService>(),
Mock.Of<RazorEngine>(),
new DefaultRazorProject(fileProvider),
GetFileProviderAccessor(fileProvider),
NullLoggerFactory.Instance);
var errors = new[]
{
GetRazorDiagnostic("message-1", new SourceLocation(1, 2, 17), length: 1),
GetRazorDiagnostic("message-2", new SourceLocation(viewPath, 1, 4, 6), length: 7),
GetRazorDiagnostic("message-3", SourceLocation.Undefined, length: -1),
GetRazorDiagnostic("message-4", new SourceLocation(viewImportsPath, 1, 3, 8), length: 4),
};
// Act
var result = razorService.GetCompilationFailedResult(viewPath, errors);
// Assert
Assert.NotNull(result.CompilationFailures);
Assert.Collection(result.CompilationFailures,
failure =>
{
Assert.Equal(viewPath, failure.SourceFilePath);
Assert.Equal("View Content", failure.SourceFileContent);
Assert.Collection(failure.Messages,
message =>
{
Assert.Equal(errors[0].GetMessage(), message.Message);
Assert.Equal(viewPath, message.SourceFilePath);
Assert.Equal(3, message.StartLine);
Assert.Equal(17, message.StartColumn);
Assert.Equal(3, message.EndLine);
Assert.Equal(18, message.EndColumn);
},
message =>
{
Assert.Equal(errors[1].GetMessage(), message.Message);
Assert.Equal(viewPath, message.SourceFilePath);
Assert.Equal(5, message.StartLine);
Assert.Equal(6, message.StartColumn);
Assert.Equal(5, message.EndLine);
Assert.Equal(13, message.EndColumn);
},
message =>
{
Assert.Equal(errors[2].GetMessage(), message.Message);
Assert.Equal(viewPath, message.SourceFilePath);
Assert.Equal(0, message.StartLine);
Assert.Equal(-1, message.StartColumn);
Assert.Equal(0, message.EndLine);
Assert.Equal(-2, message.EndColumn);
});
},
failure =>
{
Assert.Equal(viewImportsPath, failure.SourceFilePath);
Assert.Equal("Global Import Content", failure.SourceFileContent);
Assert.Collection(failure.Messages,
message =>
{
Assert.Equal(errors[3].GetMessage(), message.Message);
Assert.Equal(viewImportsPath, message.SourceFilePath);
Assert.Equal(4, message.StartLine);
Assert.Equal(8, message.StartColumn);
Assert.Equal(4, message.EndLine);
Assert.Equal(12, message.EndColumn);
});
});
}
private static IRazorViewEngineFileProviderAccessor GetFileProviderAccessor(IFileProvider fileProvider = null)
{
var options = new Mock<IRazorViewEngineFileProviderAccessor>();
options.SetupGet(o => o.FileProvider)
.Returns(fileProvider ?? new TestFileProvider());
return options.Object;
}
private static RazorDiagnostic GetRazorDiagnostic(string message, SourceLocation sourceLocation, int length)
{
var diagnosticDescriptor = new RazorDiagnosticDescriptor("test-id", () => message, RazorDiagnosticSeverity.Error);
var sourceSpan = new SourceSpan(sourceLocation, length);
return RazorDiagnostic.Create(diagnosticDescriptor, sourceSpan);
}
}
}

View File

@ -0,0 +1,262 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public class RazorCompilerTest
{
[Fact]
public void GetCompilationFailedResult_ReadsRazorErrorsFromPage()
{
// Arrange
var viewPath = "/Views/Home/Index.cshtml";
var razorEngine = RazorEngine.Create();
var fileProvider = new TestFileProvider();
fileProvider.AddFile(viewPath, "<span name=\"@(User.Id\">");
var razorProject = new DefaultRazorProject(fileProvider);
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
var compiler = new RazorCompiler(
Mock.Of<ICompilationService>(),
GetCompilerCacheProvider(fileProvider),
templateEngine);
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
// Act
var csharpDocument = templateEngine.GenerateCode(codeDocument);
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
// Assert
var failure = Assert.Single(compilationResult.CompilationFailures);
Assert.Equal(viewPath, failure.SourceFilePath);
Assert.Collection(failure.Messages,
message => Assert.StartsWith(
@"(1,22): Error RZ9999: Unterminated string literal.",
message.FormattedMessage),
message => Assert.StartsWith(
@"(1,14): Error RZ9999: The explicit expression block is missing a closing "")"" character.",
message.FormattedMessage));
}
[Fact]
public void GetCompilationFailedResult_UsesPhysicalPath()
{
// Arrange
var viewPath = "/Views/Home/Index.cshtml";
var physicalPath = @"x:\myapp\views\home\index.cshtml";
var razorEngine = RazorEngine.Create();
var fileProvider = new TestFileProvider();
var file = fileProvider.AddFile(viewPath, "<span name=\"@(User.Id\">");
file.PhysicalPath = physicalPath;
var razorProject = new DefaultRazorProject(fileProvider);
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
var compiler = new RazorCompiler(
Mock.Of<ICompilationService>(),
GetCompilerCacheProvider(fileProvider),
templateEngine);
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
// Act
var csharpDocument = templateEngine.GenerateCode(codeDocument);
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
// Assert
var failure = Assert.Single(compilationResult.CompilationFailures);
Assert.Equal(physicalPath, failure.SourceFilePath);
}
[Fact]
public void GetCompilationFailedResult_ReadsContentFromSourceDocuments()
{
// Arrange
var viewPath = "/Views/Home/Index.cshtml";
var fileContent =
@"
@if (User.IsAdmin)
{
<span>
}
</span>";
var razorEngine = RazorEngine.Create();
var fileProvider = new TestFileProvider();
fileProvider.AddFile(viewPath, fileContent);
var razorProject = new DefaultRazorProject(fileProvider);
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
var compiler = new RazorCompiler(
Mock.Of<ICompilationService>(),
GetCompilerCacheProvider(fileProvider),
templateEngine);
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
// Act
var csharpDocument = templateEngine.GenerateCode(codeDocument);
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
// Assert
var failure = Assert.Single(compilationResult.CompilationFailures);
Assert.Equal(fileContent, failure.SourceFileContent);
}
[Fact]
public void GetCompilationFailedResult_ReadsContentFromImports()
{
// Arrange
var viewPath = "/Views/Home/Index.cshtml";
var importsFilePath = @"x:\views\_MyImports.cshtml";
var fileContent = "@ ";
var importsContent = "@(abc";
var razorEngine = RazorEngine.Create();
var fileProvider = new TestFileProvider();
fileProvider.AddFile(viewPath, fileContent);
var importsFile = fileProvider.AddFile("/Views/_MyImports.cshtml", importsContent);
importsFile.PhysicalPath = importsFilePath;
var razorProject = new DefaultRazorProject(fileProvider);
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject)
{
Options =
{
ImportsFileName = "_MyImports.cshtml",
}
};
var compiler = new RazorCompiler(
Mock.Of<ICompilationService>(),
GetCompilerCacheProvider(fileProvider),
templateEngine);
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
// Act
var csharpDocument = templateEngine.GenerateCode(codeDocument);
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
// Assert
// This expectation is incorrect. https://github.com/aspnet/Razor/issues/1069 needs to be fixed,
// which should cause this test to fail.
var failure = Assert.Single(compilationResult.CompilationFailures);
Assert.Equal(viewPath, failure.SourceFilePath);
Assert.Collection(failure.Messages,
message =>
{
Assert.Equal(@"A space or line break was encountered after the ""@"" character. Only valid identifiers, keywords, comments, ""("" and ""{"" are valid at the start of a code block and they must occur immediately following ""@"" with no space in between.",
message.Message);
},
message =>
{
Assert.Equal(@"The explicit expression block is missing a closing "")"" character. Make sure you have a matching "")"" character for all the ""("" characters within this block, and that none of the "")"" characters are being interpreted as markup.",
message.Message);
});
}
[Fact]
public void GetCompilationFailedResult_GroupsMessages()
{
// Arrange
var viewPath = "views/index.razor";
var viewImportsPath = "views/global.import.cshtml";
var codeDocument = RazorCodeDocument.Create(
Create(viewPath, "View Content"),
new[] { Create(viewImportsPath, "Global Import Content") });
var diagnostics = new[]
{
GetRazorDiagnostic("message-1", new SourceLocation(1, 2, 17), length: 1),
GetRazorDiagnostic("message-2", new SourceLocation(viewPath, 1, 4, 6), length: 7),
GetRazorDiagnostic("message-3", SourceLocation.Undefined, length: -1),
GetRazorDiagnostic("message-4", new SourceLocation(viewImportsPath, 1, 3, 8), length: 4),
};
var fileProvider = new TestFileProvider();
var compiler = new RazorCompiler(
Mock.Of<ICompilationService>(),
GetCompilerCacheProvider(fileProvider),
new MvcRazorTemplateEngine(RazorEngine.Create(), new DefaultRazorProject(fileProvider)));
// Act
var result = compiler.GetCompilationFailedResult(codeDocument, diagnostics);
// Assert
Assert.Collection(result.CompilationFailures,
failure =>
{
Assert.Equal(viewPath, failure.SourceFilePath);
Assert.Equal("View Content", failure.SourceFileContent);
Assert.Collection(failure.Messages,
message =>
{
Assert.Equal(diagnostics[0].GetMessage(), message.Message);
Assert.Equal(viewPath, message.SourceFilePath);
Assert.Equal(3, message.StartLine);
Assert.Equal(17, message.StartColumn);
Assert.Equal(3, message.EndLine);
Assert.Equal(18, message.EndColumn);
},
message =>
{
Assert.Equal(diagnostics[1].GetMessage(), message.Message);
Assert.Equal(viewPath, message.SourceFilePath);
Assert.Equal(5, message.StartLine);
Assert.Equal(6, message.StartColumn);
Assert.Equal(5, message.EndLine);
Assert.Equal(13, message.EndColumn);
},
message =>
{
Assert.Equal(diagnostics[2].GetMessage(), message.Message);
Assert.Equal(viewPath, message.SourceFilePath);
Assert.Equal(0, message.StartLine);
Assert.Equal(-1, message.StartColumn);
Assert.Equal(0, message.EndLine);
Assert.Equal(-2, message.EndColumn);
});
},
failure =>
{
Assert.Equal(viewImportsPath, failure.SourceFilePath);
Assert.Equal("Global Import Content", failure.SourceFileContent);
Assert.Collection(failure.Messages,
message =>
{
Assert.Equal(diagnostics[3].GetMessage(), message.Message);
Assert.Equal(viewImportsPath, message.SourceFilePath);
Assert.Equal(4, message.StartLine);
Assert.Equal(8, message.StartColumn);
Assert.Equal(4, message.EndLine);
Assert.Equal(12, message.EndColumn);
});
});
}
private ICompilerCacheProvider GetCompilerCacheProvider(TestFileProvider fileProvider)
{
var compilerCache = new CompilerCache(fileProvider);
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider.SetupGet(p => p.Cache).Returns(compilerCache);
return compilerCacheProvider.Object;
}
private static RazorSourceDocument Create(string path, string template)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(template));
return RazorSourceDocument.ReadFrom(stream, path);
}
private static RazorDiagnostic GetRazorDiagnostic(string message, SourceLocation sourceLocation, int length)
{
var diagnosticDescriptor = new RazorDiagnosticDescriptor("test-id", () => message, RazorDiagnosticSeverity.Error);
var sourceSpan = new SourceSpan(sourceLocation, length);
return RazorDiagnostic.Create(diagnosticDescriptor, sourceSpan);
}
}
}

View File

@ -7,8 +7,10 @@ using System.Threading;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Caching.Memory;
@ -855,7 +857,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart, new IChangeToken[0]));
var viewEngine = CreateViewEngine(pageFactory.Object);
var fileProvider = new TestFileProvider();
var razorProject = new DefaultRazorProject(fileProvider);
var viewEngine = CreateViewEngine(pageFactory.Object, razorProject: razorProject);
var context = GetActionContext(_controllerTestContext);
// Act 1
@ -1302,6 +1306,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
Mock.Of<IRazorPageActivator>(),
new HtmlTestEncoder(),
GetOptionsAccessor(expanders: null),
new DefaultRazorProject(new TestFileProvider()),
loggerFactory);
// Act
@ -1615,10 +1620,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
private TestableRazorViewEngine CreateViewEngine(
IRazorPageFactoryProvider pageFactory = null,
IEnumerable<IViewLocationExpander> expanders = null)
IEnumerable<IViewLocationExpander> expanders = null,
RazorProject razorProject = null)
{
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactoryProvider>();
return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders));
if (razorProject == null)
{
razorProject = new DefaultRazorProject(new TestFileProvider());
}
return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders), razorProject);
}
private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
@ -1707,7 +1717,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
public TestableRazorViewEngine(
IRazorPageFactoryProvider pageFactory,
IOptions<RazorViewEngineOptions> optionsAccessor)
: base(pageFactory, Mock.Of<IRazorPageActivator>(), new HtmlTestEncoder(), optionsAccessor, NullLoggerFactory.Instance)
: this(pageFactory, optionsAccessor, new DefaultRazorProject(new TestFileProvider()))
{
}
public TestableRazorViewEngine(
IRazorPageFactoryProvider pageFactory,
IOptions<RazorViewEngineOptions> optionsAccessor,
RazorProject razorProject)
: base(pageFactory, Mock.Of<IRazorPageActivator>(), new HtmlTestEncoder(), optionsAccessor, razorProject, NullLoggerFactory.Instance)
{
}

View File

@ -210,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/Home/Path1/_PageStart.cshtml", "content1");
fileProvider.AddFile("/_PageStart.cshtml", "content2");
var defaultRazorProject = new DefaultRazorProject(fileProvider);
var defaultRazorProject = new TestRazorProject(fileProvider);
var invokerProvider = CreateInvokerProvider(
loader.Object,
@ -249,7 +249,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection);
var razorPageFactoryProvider = new Mock<IRazorPageFactoryProvider>();
var fileProvider = new TestFileProvider();
var defaultRazorProject = new DefaultRazorProject(fileProvider);
var defaultRazorProject = new TestRazorProject(fileProvider);
var invokerProvider = CreateInvokerProvider(
loader.Object,
@ -592,7 +592,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
fileProvider.AddFile("/Views/_PageStart.cshtml", "@page starts!");
fileProvider.AddFile("/Views/Deeper/_PageStart.cshtml", "page content");
var razorProject = CreateRazorProject(fileProvider);
var razorProject = new TestRazorProject(fileProvider);
var mock = new Mock<IRazorPageFactoryProvider>();
mock.Setup(p => p.CreateFactory("/Views/Deeper/_PageStart.cshtml"))
@ -642,8 +642,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// No files
var fileProvider = new TestFileProvider();
var razorProject = CreateRazorProject(fileProvider);
var razorProject = new TestRazorProject(fileProvider);
var invokerProvider = CreateInvokerProvider(
loader.Object,
@ -670,11 +669,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
return mock.Object;
}
private RazorProject CreateRazorProject(IFileProvider fileProvider)
{
return new DefaultRazorProject(fileProvider);
}
private static CompiledPageActionDescriptor CreateCompiledPageActionDescriptor(
PageActionDescriptor descriptor,
Type pageType = null)

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.Extensions.FileProviders;
using Moq;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
public class TestRazorProject : DefaultRazorProject
{
public TestRazorProject(IFileProvider fileProvider)
:base(GetAccessor(fileProvider))
{
}
private static IRazorViewEngineFileProviderAccessor GetAccessor(IFileProvider fileProvider)
{
var fileProviderAccessor = new Mock<IRazorViewEngineFileProviderAccessor>();
fileProviderAccessor.SetupGet(f => f.FileProvider)
.Returns(fileProvider);
return fileProviderAccessor.Object;
}
}
}

View File

@ -74,6 +74,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
}
public virtual TestFileChangeToken AddChangeToken(string filter)
{
var changeToken = new TestFileChangeToken();
_fileTriggers[filter] = changeToken;
return changeToken;
}
public virtual IChangeToken Watch(string filter)
{
TestFileChangeToken changeToken;

View File

@ -5,7 +5,7 @@ using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.DependencyInjection;
namespace RazorPageExecutionInstrumentationWebSite
@ -16,7 +16,7 @@ namespace RazorPageExecutionInstrumentationWebSite
public void ConfigureServices(IServiceCollection services)
{
// Normalize line endings to avoid changes in instrumentation locations between systems.
services.AddTransient<IRazorCompilationService, TestRazorCompilationService>();
services.AddTransient<RazorProject, TestRazorProject>();
// Add MVC services to the services container.
services.AddMvc();

View File

@ -3,40 +3,45 @@
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Logging;
namespace RazorPageExecutionInstrumentationWebSite
{
public class TestRazorCompilationService : RazorCompilationService
public class TestRazorProject : DefaultRazorProject
{
public TestRazorCompilationService(
ICompilationService compilationService,
RazorEngine engine,
RazorProject project,
IRazorViewEngineFileProviderAccessor fileProviderAccessor,
ILoggerFactory loggerFactory)
: base(compilationService, engine, project, fileProviderAccessor, loggerFactory)
public TestRazorProject(IRazorViewEngineFileProviderAccessor fileProviderAccessor)
: base(fileProviderAccessor)
{
}
public override RazorCodeDocument CreateCodeDocument(string relativePath, Stream inputStream)
public override RazorProjectItem GetItem(string path)
{
// Normalize line endings to '\r\n' (CRLF). This removes core.autocrlf, core.eol, core.safecrlf, and
// .gitattributes from the equation and treats "\r\n" and "\n" as equivalent. Does not handle
// some line endings like "\r" but otherwise ensures checksums and line mappings are consistent.
string text;
using (var streamReader = new StreamReader(inputStream))
var item = (DefaultRazorProjectItem)base.GetItem(path);
return new TestRazorProjectItem(item);
}
private class TestRazorProjectItem : DefaultRazorProjectItem
{
public TestRazorProjectItem(DefaultRazorProjectItem projectItem)
: base(projectItem.FileInfo, projectItem.BasePath, projectItem.Path)
{
text = streamReader.ReadToEnd().Replace("\r", "").Replace("\n", "\r\n");
}
var bytes = Encoding.UTF8.GetBytes(text);
inputStream = new MemoryStream(bytes);
public override Stream Read()
{
// Normalize line endings to '\r\n' (CRLF). This removes core.autocrlf, core.eol, core.safecrlf, and
// .gitattributes from the equation and treats "\r\n" and "\n" as equivalent. Does not handle
// some line endings like "\r" but otherwise ensures checksums and line mappings are consistent.
string text;
using (var streamReader = new StreamReader(base.Read()))
{
text = streamReader.ReadToEnd().Replace("\r", "").Replace("\n", "\r\n");
}
return base.CreateCodeDocument(relativePath, inputStream);
var bytes = Encoding.UTF8.GetBytes(text);
return new MemoryStream(bytes);
}
}
}
}