diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorTemplateEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorTemplateEngine.cs new file mode 100644 index 0000000000..c20dedef45 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorTemplateEngine.cs @@ -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 +{ + /// + /// A for Mvc Razor views. + /// + public class MvcRazorTemplateEngine : RazorTemplateEngine + { + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public MvcRazorTemplateEngine( + RazorEngine engine, + RazorProject project) + : base(engine, project) + { + Options.DefaultImports = GetDefaultImports(); + } + + /// + 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 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); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewHierarchyUtility.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewHierarchyUtility.cs deleted file mode 100644 index cebd4b4d02..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewHierarchyUtility.cs +++ /dev/null @@ -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 -{ - /// - /// Contains methods to locate _ViewStart.cshtml and _ViewImports.cshtml - /// - public static class ViewHierarchyUtility - { - private const string ViewStartFileName = "_ViewStart.cshtml"; - - /// - /// File name of _ViewImports.cshtml file - /// - public static readonly string ViewImportsFileName = "_ViewImports.cshtml"; - - /// - /// Gets the view start locations that are applicable to the specified path. - /// - /// The application relative path of the file to locate - /// _ViewStarts for. - /// A sequence of paths that represent potential view start locations. - /// - /// This method returns paths starting from the directory of and - /// moves upwards until it hits the application root. - /// e.g. - /// /Views/Home/View.cshtml -> [ /Views/Home/_ViewStart.cshtml, /Views/_ViewStart.cshtml, /_ViewStart.cshtml ] - /// - public static IEnumerable GetViewStartLocations(string applicationRelativePath) - { - return GetHierarchicalPath(applicationRelativePath, ViewStartFileName); - } - - /// - /// Gets the locations for _ViewImportss that are applicable to the specified path. - /// - /// The application relative path of the file to locate - /// _ViewImportss for. - /// A sequence of paths that represent potential _ViewImports locations. - /// - /// This method returns paths starting from the directory of and - /// moves upwards until it hits the application root. - /// e.g. - /// /Views/Home/View.cshtml -> [ /Views/Home/_ViewImports.cshtml, /Views/_ViewImports.cshtml, - /// /_ViewImports.cshtml ] - /// - public static IEnumerable GetViewImportsLocations(string applicationRelativePath) - { - return GetHierarchicalPath(applicationRelativePath, ViewImportsFileName); - } - - private static IEnumerable GetHierarchicalPath(string relativePath, string fileName) - { - if (string.IsNullOrEmpty(relativePath)) - { - return Enumerable.Empty(); - } - - 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(); - } - } - - var builder = new StringBuilder(relativePath); - builder.Replace('\\', '/'); - - if (builder.Length > 0 && builder[0] != '/') - { - builder.Insert(0, '/'); - } - - var locations = new List(); - 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; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IRazorCompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IRazorCompilationService.cs deleted file mode 100644 index 3826db45c2..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IRazorCompilationService.cs +++ /dev/null @@ -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 -{ - /// - /// Specifies the contracts for a service that compiles Razor files. - /// - public interface IRazorCompilationService - { - /// - /// Compiles the razor file located at . - /// - /// A instance that represents the file to compile. - /// - /// - /// A that represents the results of parsing and compiling the file. - /// - CompilationResult Compile(RelativeFileInfo fileInfo); - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RelativeFileInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RelativeFileInfo.cs deleted file mode 100644 index ed3739bf94..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RelativeFileInfo.cs +++ /dev/null @@ -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 -{ - /// - /// A container type that represents along with the application base relative path - /// for a file in the file system. - /// - public class RelativeFileInfo - { - /// - /// Initializes a new instance of . - /// - /// for the file. - /// Path of the file relative to the application base. - 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; - } - - /// - /// Gets the associated with this instance of . - /// - public IFileInfo FileInfo { get; } - - /// - /// Gets the path of the file relative to the application base. - /// - public string RelativePath { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 19e897f5e9..cabcbbb0c6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -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(); - services.TryAddTransient(); - services.TryAddSingleton(s => - { - return new DefaultRazorProject(s.GetRequiredService().FileProvider); - }); + services.TryAddSingleton(); services.TryAddSingleton(s => { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs index 5a91909aae..dfb5607e01 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs @@ -67,16 +67,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// public CompilerCacheResult GetOrAdd( string relativePath, - Func compile) + Func 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 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 CreateCacheEntry( - string relativePath, string normalizedPath, - Func compile) + Func cacheContextFactory) { TaskCompletionSource compilationTaskSource = null; - MemoryCacheEntryOptions cacheEntryOptions = null; - IFileInfo fileInfo = null; + MemoryCacheEntryOptions cacheEntryOptions; Task 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(); + foreach (var projectItem in compilerCacheContext.AdditionalCompilationItems) + { + cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(projectItem.Path)); + } cacheEntry = compilationTaskSource.Task; } - cacheEntry = _cache.Set>(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); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs new file mode 100644 index 0000000000..1dff582dce --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs @@ -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 additionalCompilationItems, + Func compile) + { + ProjectItem = projectItem; + AdditionalCompilationItems = additionalCompilationItems; + Compile = compile; + } + + public RazorProjectItem ProjectItem { get; } + + public IEnumerable AdditionalCompilationItems { get; } + + public Func Compile { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs index df8a222434..a1f444721c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs @@ -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 { /// /// Initializes a new instance of with the specified - /// . + /// . /// /// Path of the view file relative to the application base. - /// The . - public CompilerCacheResult(string relativePath, CompilationResult compilationResult) - : this(relativePath, compilationResult, EmptyArray.Instance) - { - } - - /// - /// Initializes a new instance of with the specified - /// . - /// - /// Path of the view file relative to the application base. - /// The . + /// The . /// true if the view is precompiled, false otherwise. public CompilerCacheResult(string relativePath, CompilationResult compilationResult, bool isPrecompiled) : this(relativePath, compilationResult, EmptyArray.Instance) @@ -42,10 +29,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// /// Initializes a new instance of with the specified - /// . + /// . /// /// Path of the view file relative to the application base. - /// The . + /// The . /// One or more instances that indicate when /// this result has expired. public CompilerCacheResult(string relativePath, CompilationResult compilationResult, IList 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>(objectInitializeExpression) - .Compile(); IsPrecompiled = false; } @@ -74,9 +52,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// Initializes a new instance of for a file that could not be /// found in the file system. /// + /// Path of the view file relative to the application base. /// One or more instances that indicate when /// this result has expired. - public CompilerCacheResult(IList expirationTokens) + public CompilerCacheResult(string relativePath, IList 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 /// /// Gets a value that determines if the view was successfully found and compiled. /// - public bool Success => PageFactory != null; + public bool Success => CompiledType != null; /// - /// Gets a delegate that creates an instance of the . + /// Normalized relative path of the file. /// - public Func PageFactory { get; } + public string RelativePath { get; } + + /// + /// The compiled . + /// + public Type CompiledType { get; } /// /// Gets a value that determines if the view is precompiled. diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs index 4b26111089..8c122feb23 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs @@ -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 /// public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider { - /// - /// This delegate holds on to an instance of . - /// - private readonly Func _compileDelegate; - private readonly ICompilerCacheProvider _compilerCacheProvider; - private ICompilerCache _compilerCache; + private const string ViewImportsFileName = "_ViewImports.cshtml"; + private readonly RazorCompiler _razorCompiler; /// /// Initializes a new instance of . /// - /// The . + /// The . + /// The . + /// The . /// The . 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); } /// @@ -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>(objectInitializeExpression) + .Compile(); + return new RazorPageFactoryResult(pageFactory, result.ExpirationTokens, result.IsPrecompiled); } else { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs index 3508f90bca..c6096be4c4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs @@ -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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs index 13cd73239b..a5d42b2916 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs @@ -20,6 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// A cached . CompilerCacheResult GetOrAdd( string relativePath, - Func compile); + Func compile); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompilationService.cs deleted file mode 100644 index 4fafa26f4a..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompilationService.cs +++ /dev/null @@ -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 -{ - /// - /// Default implementation of . - /// - public class RazorCompilationService : IRazorCompilationService - { - private readonly ICompilationService _compilationService; - private readonly RazorEngine _engine; - private readonly RazorProject _project; - private readonly IFileProvider _fileProvider; - private readonly ILogger _logger; - - /// - /// Instantiates a new instance of the class. - /// - /// The to compile generated code. - /// The to generate code from Razor files. - /// The implementation for locating files. - /// The . - /// The . - public RazorCompilationService( - ICompilationService compilationService, - RazorEngine engine, - RazorProject project, - IRazorViewEngineFileProviderAccessor fileProviderAccessor, - ILoggerFactory loggerFactory) - { - _compilationService = compilationService; - _engine = engine; - _fileProvider = fileProviderAccessor.FileProvider; - _logger = loggerFactory.CreateLogger(); - - _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 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; } - - /// - 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() - { - 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 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(); - 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; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs new file mode 100644 index 0000000000..873901b1f5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs @@ -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 _getCacheContext; + private readonly Func _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 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(); + 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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index b036a6d5bd..e1bf657ac1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -58,22 +58,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("FlushPointCannotBeInvoked"), p0); } - /// - /// The {0} returned by '{1}' must be an instance of '{2}'. - /// - internal static string Instrumentation_WriterMustBeBufferedTextWriter - { - get { return GetString("Instrumentation_WriterMustBeBufferedTextWriter"); } - } - - /// - /// The {0} returned by '{1}' must be an instance of '{2}'. - /// - internal static string FormatInstrumentation_WriterMustBeBufferedTextWriter(object p0, object p1, object p2) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Instrumentation_WriterMustBeBufferedTextWriter"), p0, p1, p2); - } - /// /// The layout view '{0}' could not be located. The following locations were searched:{1} /// @@ -314,70 +298,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1); } - /// - /// '{0}' must be a {1} that is generated as result of the call to '{2}'. - /// - internal static string ViewLocationCache_KeyMustBeString - { - get { return GetString("ViewLocationCache_KeyMustBeString"); } - } - - /// - /// '{0}' must be a {1} that is generated as result of the call to '{2}'. - /// - internal static string FormatViewLocationCache_KeyMustBeString(object p0, object p1, object p2) - { - return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationCache_KeyMustBeString"), p0, p1, p2); - } - - /// - /// The '{0}' method must be called before '{1}' can be invoked. - /// - internal static string ViewMustBeContextualized - { - get { return GetString("ViewMustBeContextualized"); } - } - - /// - /// The '{0}' method must be called before '{1}' can be invoked. - /// - internal static string FormatViewMustBeContextualized(object p0, object p1) - { - return string.Format(CultureInfo.CurrentCulture, GetString("ViewMustBeContextualized"), p0, p1); - } - - /// - /// Unsupported hash algorithm. - /// - internal static string RazorHash_UnsupportedHashAlgorithm - { - get { return GetString("RazorHash_UnsupportedHashAlgorithm"); } - } - - /// - /// Unsupported hash algorithm. - /// - internal static string FormatRazorHash_UnsupportedHashAlgorithm() - { - return GetString("RazorHash_UnsupportedHashAlgorithm"); - } - - /// - /// The resource '{0}' specified by '{1}' could not be found. - /// - internal static string RazorFileInfoCollection_ResourceCouldNotBeFound - { - get { return GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"); } - } - - /// - /// The resource '{0}' specified by '{1}' could not be found. - /// - internal static string FormatRazorFileInfoCollection_ResourceCouldNotBeFound(object p0, object p1) - { - return string.Format(CultureInfo.CurrentCulture, GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"), p0, p1); - } - /// /// Generated Code /// @@ -526,6 +446,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor return GetString("RazorProject_PathMustStartWithForwardSlash"); } + /// + /// The property '{0}' of '{1}' must not be null. + /// + internal static string PropertyMustBeSet + { + get { return GetString("PropertyMustBeSet"); } + } + + /// + /// The property '{0}' of '{1}' must not be null. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs index 3509690f3c..b5f00e46b3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs @@ -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; /// /// Initializes a new instance of the . @@ -51,6 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor IRazorPageActivator pageActivator, HtmlEncoder htmlEncoder, IOptions optionsAccessor, + RazorProject razorProject, ILoggerFactory loggerFactory) { _options = optionsAccessor.Value; @@ -73,6 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor _pageActivator = pageActivator; _htmlEncoder = htmlEncoder; _logger = loggerFactory.CreateLogger(); + _razorProject = razorProject; ViewLookupCache = new MemoryCache(new MemoryCacheOptions { CompactOnMemoryPressure = false @@ -483,10 +488,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor string path, HashSet expirationTokens) { + var applicationRelativePath = MakePathApplicationRelative(path); var viewStartPages = new List(); - 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)); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index 44bc4d7d75..edcc139e30 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -126,9 +126,6 @@ '{0}' cannot be invoked when a Layout page is set to be executed. - - The {0} returned by '{1}' must be an instance of '{2}'. - The layout view '{0}' could not be located. The following locations were searched:{1} @@ -174,18 +171,6 @@ '{0} must be set to access '{1}'. - - '{0}' must be a {1} that is generated as result of the call to '{2}'. - - - The '{0}' method must be called before '{1}' can be invoked. - - - Unsupported hash algorithm. - - - The resource '{0}' specified by '{1}' could not be found. - Generated Code @@ -215,4 +200,7 @@ Path must begin with a forward slash '/'. + + The property '{0}' of '{1}' must not be null. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs index 68d69634c2..5d9a9cc0c4 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs @@ -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 { + /// + /// Creates a from a . + /// public interface IPageLoader { + /// + /// Produces a given a . + /// + /// The . + /// The . CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs index e50be6ad58..ed12fbfac3 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -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 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() - { - _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; - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorTemplateEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorTemplateEngineTest.cs new file mode 100644 index 0000000000..b55ef321a6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorTemplateEngineTest.cs @@ -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 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(); + fileProviderAccessor.SetupGet(f => f.FileProvider) + .Returns(fileProvider); + + return new DefaultRazorProject(fileProviderAccessor.Object); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewHierarchyUtilityTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewHierarchyUtilityTest.cs deleted file mode 100644 index 86159c234d..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewHierarchyUtilityTest.cs +++ /dev/null @@ -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); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs index 5e5e4885b1..cf9f9f4296 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs @@ -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 _precompiledViews = new Dictionary { { PrecompiledViewsPath, typeof(PreCompile) } }; - public static TheoryData ViewImportsPaths => - new TheoryData + public static TheoryData ViewImportsPaths + { + get { - "/Views/Home/_ViewImports.cshtml", - "/Views/_ViewImports.cshtml", - "/_ViewImports.cshtml", - }; + var theoryData = new TheoryData(); + 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(), + _ => 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(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(result1.PageFactory()); + Assert.Equal(typeof(TestView), result1.CompiledType); // Act - 2 var result2 = cache.GetOrAdd(relativePath, ThrowsIfCalled); // Assert - 2 - Assert.IsType(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(), _ => expected); // Act 1 - var result1 = cache.GetOrAdd(ViewPath, _ => expected); + var result1 = cache.GetOrAdd(ViewPath, _ => cacheContext); // Assert 1 Assert.True(result1.Success); - Assert.IsType(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(), + _ => 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(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(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(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(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(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(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(result1.PageFactory()); + Assert.Equal(typeof(TestView), result1.CompiledType); // Act 2 var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); // Assert 2 Assert.True(result2.Success); - Assert.IsType(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(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(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(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(result1.PageFactory()); + Assert.Equal(typeof(TestView), result1.CompiledType); // Act 2 var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); // Assert 2 Assert.True(result2.Success); - Assert.IsType(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(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(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(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 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 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(), 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(), compile2); }); }); @@ -416,24 +457,30 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var resetEvent2 = new ManualResetEvent(initialState: false); var cache = new CompilerCache(fileProvider); + Func 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(), 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(() => - 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(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(() => cache.GetOrAdd(ViewPath, _ => compilationResult)); + var ex = Assert.Throws(() => 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(), + _ => throw exception); + } + + private Func 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(); + foreach (var importFilePath in _viewImportsPath) + { + var importProjectItem = new DefaultRazorProjectItem(new TestFileInfo(), "", importFilePath); + + imports.Add(importProjectItem); + } + + return new CompilerCacheContext(projectItem, imports, _ => compile); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs index 326042190b..c31d8562f1 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs @@ -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(), @@ -23,18 +25,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal }; var compilerCache = new Mock(); compilerCache - .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) - .Returns(new CompilerCacheResult(expirationTokens)); + .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) + .Returns(new CompilerCacheResult(path, expirationTokens)); var compilerCacheProvider = new Mock(); compilerCacheProvider .SetupGet(c => c.Cache) .Returns(compilerCache.Object); var factoryProvider = new DefaultRazorPageFactoryProvider( - Mock.Of(), + RazorEngine.Create(), + new DefaultRazorProject(new TestFileProvider()), + Mock.Of(), 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(); compilerCache - .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) + .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) .Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), expirationTokens)); var compilerCacheProvider = new Mock(); compilerCacheProvider .SetupGet(c => c.Cache) .Returns(compilerCache.Object); var factoryProvider = new DefaultRazorPageFactoryProvider( - Mock.Of(), + RazorEngine.Create(), + new DefaultRazorProject(new TestFileProvider()), + Mock.Of(), compilerCacheProvider.Object); // Act @@ -78,14 +84,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var relativePath = "/file-exists"; var compilerCache = new Mock(); compilerCache - .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) + .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) .Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), new IChangeToken[0])); var compilerCacheProvider = new Mock(); compilerCacheProvider .SetupGet(c => c.Cache) .Returns(compilerCache.Object); var factoryProvider = new DefaultRazorPageFactoryProvider( - Mock.Of(), + RazorEngine.Create(), + new DefaultRazorProject(new TestFileProvider()), + Mock.Of(), compilerCacheProvider.Object); // Act diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilationServiceTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilationServiceTest.cs deleted file mode 100644 index f7cc60b7c4..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilationServiceTest.cs +++ /dev/null @@ -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(); - 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(); - compiler.Setup(c => c.Compile(It.IsAny(), It.IsAny())) - .Returns(new CompilationResult(typeof(RazorCompilationServiceTest))); - - var engine = new Mock(); - engine.Setup(e => e.Process(It.IsAny())) - .Callback(document => - { - document.SetCSharpDocument(new RazorCSharpDocument() - { - Diagnostics = new List() - }); - - 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(); - 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(MockBehavior.Strict); - - var engine = new Mock(); - engine.Setup(e => e.Process(It.IsAny())) - .Callback(document => - { - document.SetCSharpDocument(new RazorCSharpDocument() - { - Diagnostics = new List() - { - 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(); - 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(); - compiler.Setup(c => c.Compile(It.IsAny(), It.IsAny())) - .Returns(compilationResult) - .Verifiable(); - - var engine = new Mock(); - engine.Setup(e => e.Process(It.IsAny())) - .Callback(document => - { - document.SetCSharpDocument(new RazorCSharpDocument() - { - Diagnostics = new List() - }); - }); - - 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(), - Mock.Of(), - 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(); - 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); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilerTest.cs new file mode 100644 index 0000000000..764dbfd823 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilerTest.cs @@ -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, ""); + var razorProject = new DefaultRazorProject(fileProvider); + + var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject); + var compiler = new RazorCompiler( + Mock.Of(), + 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, ""); + file.PhysicalPath = physicalPath; + var razorProject = new DefaultRazorProject(fileProvider); + + var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject); + var compiler = new RazorCompiler( + Mock.Of(), + 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) +{ + +} +"; + + 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(), + 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(), + 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(), + 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(); + 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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs index a2fffe78ab..9cc62636f4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -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(), 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 expanders = null) + IEnumerable expanders = null, + RazorProject razorProject = null) { pageFactory = pageFactory ?? Mock.Of(); - return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders)); + if (razorProject == null) + { + razorProject = new DefaultRazorProject(new TestFileProvider()); + } + return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders), razorProject); } private static IOptions GetOptionsAccessor( @@ -1707,7 +1717,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test public TestableRazorViewEngine( IRazorPageFactoryProvider pageFactory, IOptions optionsAccessor) - : base(pageFactory, Mock.Of(), new HtmlTestEncoder(), optionsAccessor, NullLoggerFactory.Instance) + : this(pageFactory, optionsAccessor, new DefaultRazorProject(new TestFileProvider())) + { + } + + public TestableRazorViewEngine( + IRazorPageFactoryProvider pageFactory, + IOptions optionsAccessor, + RazorProject razorProject) + : base(pageFactory, Mock.Of(), new HtmlTestEncoder(), optionsAccessor, razorProject, NullLoggerFactory.Instance) { } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 2007250104..f6a61b7bcb 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -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(); 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(); 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) diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/TestRazorProject.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/TestRazorProject.cs new file mode 100644 index 0000000000..c7ab938f33 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/TestRazorProject.cs @@ -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(); + fileProviderAccessor.SetupGet(f => f.FileProvider) + .Returns(fileProvider); + + return fileProviderAccessor.Object; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs index c432ff5ad3..d6efad2070 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs @@ -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; diff --git a/test/WebSites/RazorPageExecutionInstrumentationWebSite/Startup.cs b/test/WebSites/RazorPageExecutionInstrumentationWebSite/Startup.cs index 3341cc32a8..7d9689769a 100644 --- a/test/WebSites/RazorPageExecutionInstrumentationWebSite/Startup.cs +++ b/test/WebSites/RazorPageExecutionInstrumentationWebSite/Startup.cs @@ -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(); + services.AddTransient(); // Add MVC services to the services container. services.AddMvc(); diff --git a/test/WebSites/RazorPageExecutionInstrumentationWebSite/TestRazorCompilationService.cs b/test/WebSites/RazorPageExecutionInstrumentationWebSite/TestRazorCompilationService.cs index 17482281e1..98c4842fe9 100644 --- a/test/WebSites/RazorPageExecutionInstrumentationWebSite/TestRazorCompilationService.cs +++ b/test/WebSites/RazorPageExecutionInstrumentationWebSite/TestRazorCompilationService.cs @@ -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); + } } } } \ No newline at end of file