From 452578e4a880676a35a8c50a6e4e8e11dccbd125 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 May 2017 11:07:58 -0700 Subject: [PATCH] Revisit the architecture of CompilerCache Fixes #5912 --- .../Compilation/CompilationResult.cs | 72 -- .../Compilation/CompiledViewDescriptor.cs | 28 + .../Compilation/ICompilationService.cs | 27 - .../Compilation/IViewCompiler.cs | 12 + .../Compilation/IViewCompilerProvider.cs | 10 + .../Compilation/RazorViewAttribute.cs | 27 + .../Compilation/ViewsFeature.cs | 4 +- .../Compilation/ViewsFeatureProvider.cs | 13 +- .../MvcRazorMvcCoreBuilderExtensions.cs | 6 +- .../CompilationFailedExceptionFactory.cs | 159 +++++ .../Internal/CompilerCache.cs | 194 ------ .../Internal/CompilerCacheContext.cs | 29 - .../Internal/CompilerCacheResult.cs | 96 --- .../Internal/DefaultCompilerCacheProvider.cs | 31 - .../DefaultRazorPageFactoryProvider.cs | 24 +- .../DefaultRoslynCompilationService.cs | 228 ------ .../Internal/ICompilerCache.cs | 25 - .../Internal/ICompilerCacheProvider.cs | 16 - .../Internal/RazorCompiler.cs | 133 ---- .../Internal/RazorViewCompiler.cs | 267 +++++++ .../Internal/RazorViewCompilerProvider.cs | 80 +++ .../Internal/ViewPath.cs | 45 ++ .../ApplicationParts/CompiledPageFeature.cs | 12 - .../CompiledPageFeatureProvider.cs | 39 +- .../Infrastructure/RazorPageAttribute.cs | 22 + .../CompiledPageApplicationModelProvider.cs | 19 +- .../Internal/DefaultPageLoader.cs | 40 +- .../Compilation/CompilationResultTest.cs | 32 - .../Compilation/ViewsFeatureProviderTest.cs | 20 +- .../Internal/CSharpCompilerTest.cs | 43 ++ .../Internal/CompilerCacheTest.cs | 656 ------------------ ... => CompilerFailedExceptionFactoryTest.cs} | 129 +++- .../DefaultRazorPageFactoryProviderTest.cs | 60 +- .../DefaultRoslynCompilationServiceTest.cs | 350 ---------- .../Internal/RazorViewCompilerProviderTest.cs | 44 ++ .../Internal/RazorViewCompilerTest.cs | 527 ++++++++++++++ .../Internal/ReferenceManagerTest.cs | 15 +- .../Internal/ViewPathTest.cs | 36 + ...ompiledPageApplicationModelProviderTest.cs | 50 +- .../Internal/DefaultPageLoaderTest.cs | 26 +- 40 files changed, 1590 insertions(+), 2056 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompilationResult.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ICompilationService.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompiler.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompilerProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewAttribute.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilationFailedExceptionFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCacheProvider.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompilerProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewPath.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAttribute.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/CompilationResultTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs rename test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/{RazorCompilerTest.cs => CompilerFailedExceptionFactoryTest.cs} (67%) delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ViewPathTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompilationResult.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompilationResult.cs deleted file mode 100644 index 2f78c3ae5a..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompilationResult.cs +++ /dev/null @@ -1,72 +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 Microsoft.AspNetCore.Diagnostics; - -namespace Microsoft.AspNetCore.Mvc.Razor.Compilation -{ - /// - /// Represents the result of compilation. - /// - public struct CompilationResult - { - /// - /// Initializes a new instance of for a successful compilation. - /// - /// The compiled type. - public CompilationResult(Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - CompiledType = type; - CompilationFailures = null; - } - - /// - /// Initializes a new instance of for a failed compilation. - /// - /// s produced from parsing or - /// compiling the Razor file. - public CompilationResult(IEnumerable compilationFailures) - { - if (compilationFailures == null) - { - throw new ArgumentNullException(nameof(compilationFailures)); - } - - CompiledType = null; - CompilationFailures = compilationFailures; - } - - /// - /// Gets the type produced as a result of compilation. - /// - /// This property is null when compilation failed. - public Type CompiledType { get; } - - /// - /// Gets the s produced from parsing or compiling the Razor file. - /// - /// This property is null when compilation succeeded. An empty sequence - /// indicates a failed compilation. - public IEnumerable CompilationFailures { get; } - - /// - /// Gets the . - /// - /// The current instance. - /// Thrown if compilation failed. - public void EnsureSuccessful() - { - if (CompilationFailures != null) - { - throw new CompilationFailedException(CompilationFailures); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs new file mode 100644 index 0000000000..34514536e7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public class CompiledViewDescriptor + { + public string RelativePath { get; set; } + + /// + /// Gets or sets the decorating the sview. + /// + public RazorViewAttribute ViewAttribute { get; set; } + + /// + /// instances that indicate when this result has expired. + /// + public IList ExpirationTokens { get; set; } + + /// + /// Gets a value that determines if the view is precompiled. + /// + public bool IsPrecompiled { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ICompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ICompilationService.cs deleted file mode 100644 index 0a44a75635..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ICompilationService.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.AspNetCore.Mvc.Razor.Compilation -{ - /// - /// Provides methods for compilation of a Razor page. - /// - public interface ICompilationService - { - /// - /// Compiles a and returns the result of compilation. - /// - /// - /// The that contains the sources for the compilation. - /// - /// - /// The to compile. - /// - /// - /// A representing the result of compilation. - /// - CompilationResult Compile(RazorCodeDocument codeDocument, RazorCSharpDocument cSharpDocument); - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompiler.cs new file mode 100644 index 0000000000..f861fa8c58 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompiler.cs @@ -0,0 +1,12 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public interface IViewCompiler + { + Task CompileAsync(string relativePath); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompilerProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompilerProvider.cs new file mode 100644 index 0000000000..34c65481f6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/IViewCompilerProvider.cs @@ -0,0 +1,10 @@ +// 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 +{ + public interface IViewCompilerProvider + { + IViewCompiler GetCompiler(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewAttribute.cs new file mode 100644 index 0000000000..1ac6aba09a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewAttribute.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class RazorViewAttribute : Attribute + { + public RazorViewAttribute(string path, Type viewType) + { + Path = path; + ViewType = viewType; + } + + /// + /// Gets the path of the view. + /// + public string Path { get; } + + /// + /// Gets the view type. + /// + public Type ViewType { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs index 3f1b5045ae..dde65a6340 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs @@ -1,14 +1,12 @@ // 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; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { public class ViewsFeature { - public IDictionary Views { get; } = - new Dictionary(StringComparer.OrdinalIgnoreCase); + public IList ViewDescriptors { get; } = new List(); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs index a87b02dde3..4253b899f6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { @@ -40,7 +42,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation foreach (var item in viewContainer.ViewInfos) { - feature.Views[item.Path] = item.Type; + var relativePath = ViewPath.NormalizePath(item.Path); + var viewDescriptor = new CompiledViewDescriptor + { + ExpirationTokens = Array.Empty(), + RelativePath = relativePath, + ViewAttribute = new RazorViewAttribute(relativePath, item.Type), + IsPrecompiled = true, + }; + + feature.ViewDescriptors.Add(viewDescriptor); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index f4a89f9587..7663748620 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -134,7 +134,6 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); // This caches compilation related details that are valid across the lifetime of the application. - services.TryAddSingleton(); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcRazorMvcViewOptionsSetup>()); @@ -153,9 +152,7 @@ namespace Microsoft.Extensions.DependencyInjection DefaultRazorViewEngineFileProviderAccessor>(); services.TryAddSingleton(); - - // Caches compilation artifacts across the lifetime of the application. - services.TryAddSingleton(); + services.TryAddSingleton(); // In the default scenario the following services are singleton by virtue of being initialized as part of // creating the singleton RazorViewEngine instance. @@ -166,7 +163,6 @@ namespace Microsoft.Extensions.DependencyInjection // services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(s => diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilationFailedExceptionFactory.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilationFailedExceptionFactory.cs new file mode 100644 index 0000000000..a24079d6ec --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilationFailedExceptionFactory.cs @@ -0,0 +1,159 @@ +// 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.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + internal static class CompilationFailedExceptionFactory + { + // error CS0234: The type or namespace name 'C' does not exist in the namespace 'N' (are you missing + // an assembly reference?) + private const string CS0234 = nameof(CS0234); + // error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive + // or an assembly reference?) + private const string CS0246 = nameof(CS0246); + + public static CompilationFailedException Create( + RazorCodeDocument codeDocument, + IEnumerable 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 CompilationFailedException(failures); + } + + public static CompilationFailedException Create( + RazorCodeDocument codeDocument, + string compilationContent, + string assemblyName, + IEnumerable diagnostics) + { + var diagnosticGroups = diagnostics + .Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error) + .GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal); + + var failures = new List(); + foreach (var group in diagnosticGroups) + { + var sourceFilePath = group.Key; + string sourceFileContent; + if (string.Equals(assemblyName, sourceFilePath, StringComparison.Ordinal)) + { + // The error is in the generated code and does not have a mapping line pragma + sourceFileContent = compilationContent; + sourceFilePath = Resources.GeneratedCodeFileName; + } + else + { + sourceFileContent = ReadContent(codeDocument, sourceFilePath); + } + + string additionalMessage = null; + if (group.Any(g => + string.Equals(CS0234, g.Id, StringComparison.OrdinalIgnoreCase) || + string.Equals(CS0246, g.Id, StringComparison.OrdinalIgnoreCase))) + { + additionalMessage = Resources.FormatCompilation_DependencyContextIsNotSpecified( + "Microsoft.NET.Sdk.Web", + "PreserveCompilationContext"); + } + + var compilationFailure = new CompilationFailure( + sourceFilePath, + sourceFileContent, + compilationContent, + group.Select(GetDiagnosticMessage), + additionalMessage); + + failures.Add(compilationFailure); + } + + return new CompilationFailedException(failures); + } + + private static string ReadContent(RazorCodeDocument codeDocument, string filePath) + { + RazorSourceDocument sourceDocument = 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 GetDiagnosticMessage(Diagnostic diagnostic) + { + var mappedLineSpan = diagnostic.Location.GetMappedLineSpan(); + return new DiagnosticMessage( + diagnostic.GetMessage(), + CSharpDiagnosticFormatter.Instance.Format(diagnostic), + mappedLineSpan.Path, + mappedLineSpan.StartLinePosition.Line + 1, + mappedLineSpan.StartLinePosition.Character + 1, + mappedLineSpan.EndLinePosition.Line + 1, + mappedLineSpan.EndLinePosition.Character + 1); + } + + private static DiagnosticMessage CreateDiagnosticMessage( + RazorDiagnostic razorDiagnostic, + string filePath) + { + var sourceSpan = razorDiagnostic.Span; + var message = razorDiagnostic.GetMessage(); + return new DiagnosticMessage( + message: message, + formattedMessage: razorDiagnostic.ToString(), + filePath: filePath, + startLine: sourceSpan.LineIndex + 1, + startColumn: sourceSpan.CharacterIndex, + endLine: sourceSpan.LineIndex + 1, + endColumn: sourceSpan.CharacterIndex + sourceSpan.Length); + } + + private static string GetFilePath(RazorCodeDocument codeDocument, Diagnostic diagnostic) + { + if (diagnostic.Location == Location.None) + { + return codeDocument.Source.FileName; + } + + return diagnostic.Location.GetMappedLineSpan().Path; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs deleted file mode 100644 index 61649cc6fd..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs +++ /dev/null @@ -1,194 +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.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.FileProviders; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - /// - /// Caches the result of runtime compilation of Razor files for the duration of the application lifetime. - /// - public class CompilerCache : ICompilerCache - { - private readonly IFileProvider _fileProvider; - private readonly IMemoryCache _cache; - private readonly object _cacheLock = new object(); - - private readonly ConcurrentDictionary _normalizedPathLookup = - new ConcurrentDictionary(StringComparer.Ordinal); - - /// - /// Initializes a new instance of . - /// - /// used to locate Razor views. - public CompilerCache(IFileProvider fileProvider) - { - if (fileProvider == null) - { - throw new ArgumentNullException(nameof(fileProvider)); - } - - _fileProvider = fileProvider; - _cache = new MemoryCache(new MemoryCacheOptions()); - } - - /// - /// Initializes a new instance of populated with precompiled views - /// specified by . - /// - /// used to locate Razor views. - /// A mapping of application relative paths of view to s that - /// have already been compiled. - public CompilerCache( - IFileProvider fileProvider, - IDictionary views) - : this(fileProvider) - { - if (views == null) - { - throw new ArgumentNullException(nameof(views)); - } - - foreach (var item in views) - { - var cacheEntry = new CompilerCacheResult(item.Key, new CompilationResult(item.Value), isPrecompiled: true); - _cache.Set(GetNormalizedPath(item.Key), Task.FromResult(cacheEntry)); - } - } - - /// - public CompilerCacheResult GetOrAdd( - string relativePath, - Func cacheContextFactory) - { - if (relativePath == null) - { - throw new ArgumentNullException(nameof(relativePath)); - } - - if (cacheContextFactory == null) - { - throw new ArgumentNullException(nameof(cacheContextFactory)); - } - - Task cacheEntry; - // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already - // normalized and a cache entry exists. - if (!_cache.TryGetValue(relativePath, out cacheEntry)) - { - var normalizedPath = GetNormalizedPath(relativePath); - if (!_cache.TryGetValue(normalizedPath, out cacheEntry)) - { - cacheEntry = CreateCacheEntry(normalizedPath, cacheContextFactory); - } - } - - // The Task does not represent async work and is meant to provide per-entry locking. - // Hence it is ok to perform .GetResult() to read the result. - return cacheEntry.GetAwaiter().GetResult(); - } - - private Task CreateCacheEntry( - string normalizedPath, - Func cacheContextFactory) - { - TaskCompletionSource compilationTaskSource = 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 - // actual work for compiling files happens outside the critical section. - lock (_cacheLock) - { - if (_cache.TryGetValue(normalizedPath, out cacheEntry)) - { - return cacheEntry; - } - - if (_fileProvider is NullFileProvider) - { - var message = Resources.FormatFileProvidersAreRequired( - typeof(RazorViewEngineOptions).FullName, - nameof(RazorViewEngineOptions.FileProviders), - typeof(IFileProvider).FullName); - throw new InvalidOperationException(message); - } - - cacheEntryOptions = new MemoryCacheEntryOptions(); - - compilerCacheContext = cacheContextFactory(normalizedPath); - cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(compilerCacheContext.ProjectItem.Path)); - if (!compilerCacheContext.ProjectItem.Exists) - { - cacheEntry = Task.FromResult(new CompilerCacheResult(normalizedPath, cacheEntryOptions.ExpirationTokens)); - } - else - { - // 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); - } - - if (compilationTaskSource != null) - { - // Indicates that a file was found and needs to be compiled. - Debug.Assert(cacheEntryOptions != null); - - try - { - var compilationResult = compilerCacheContext.Compile(compilerCacheContext); - compilationResult.EnsureSuccessful(); - compilationTaskSource.SetResult( - new CompilerCacheResult(normalizedPath, compilationResult, cacheEntryOptions.ExpirationTokens)); - } - catch (Exception ex) - { - compilationTaskSource.SetException(ex); - } - } - - return cacheEntry; - } - - private string GetNormalizedPath(string relativePath) - { - Debug.Assert(relativePath != null); - if (relativePath.Length == 0) - { - return relativePath; - } - - string normalizedPath; - if (!_normalizedPathLookup.TryGetValue(relativePath, out normalizedPath)) - { - var builder = new StringBuilder(relativePath); - builder.Replace('\\', '/'); - if (builder[0] != '/') - { - builder.Insert(0, '/'); - } - normalizedPath = builder.ToString(); - _normalizedPathLookup.TryAdd(relativePath, normalizedPath); - } - - return normalizedPath; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs deleted file mode 100644 index 721ca439d7..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheContext.cs +++ /dev/null @@ -1,29 +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 Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.AspNetCore.Razor.Language; - -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 deleted file mode 100644 index 0bca7fc8dc..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs +++ /dev/null @@ -1,96 +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 Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - /// - /// Result of . - /// - public struct CompilerCacheResult - { - /// - /// Initializes a new instance of with the specified - /// . - /// - /// Path of the view file relative to the application base. - /// The . - /// true if the view is precompiled, false otherwise. - public CompilerCacheResult(string relativePath, CompilationResult compilationResult, bool isPrecompiled) - : this(relativePath, compilationResult, Array.Empty()) - { - IsPrecompiled = isPrecompiled; - } - - /// - /// Initializes a new instance of with the specified - /// . - /// - /// Path of the view file relative to the application base. - /// The . - /// One or more instances that indicate when - /// this result has expired. - public CompilerCacheResult(string relativePath, CompilationResult compilationResult, IList expirationTokens) - { - if (expirationTokens == null) - { - throw new ArgumentNullException(nameof(expirationTokens)); - } - - RelativePath = relativePath; - CompiledType = compilationResult.CompiledType; - ExpirationTokens = expirationTokens; - IsPrecompiled = false; - } - - /// - /// 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(string relativePath, IList expirationTokens) - { - if (expirationTokens == null) - { - throw new ArgumentNullException(nameof(expirationTokens)); - } - - ExpirationTokens = expirationTokens; - RelativePath = null; - CompiledType = null; - IsPrecompiled = false; - } - - /// - /// instances that indicate when this result has expired. - /// - public IList ExpirationTokens { get; } - - /// - /// Gets a value that determines if the view was successfully found and compiled. - /// - public bool Success => CompiledType != null; - - /// - /// Normalized relative path of the file. - /// - public string RelativePath { get; } - - /// - /// The compiled . - /// - public Type CompiledType { get; } - - /// - /// Gets a value that determines if the view is precompiled. - /// - public bool IsPrecompiled { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs deleted file mode 100644 index ba328ca136..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - /// - /// Default implementation for . - /// - public class DefaultCompilerCacheProvider : ICompilerCacheProvider - { - /// - /// Initializes a new instance of . - /// - /// The - /// The . - public DefaultCompilerCacheProvider( - ApplicationPartManager applicationPartManager, - IRazorViewEngineFileProviderAccessor fileProviderAccessor) - { - var feature = new ViewsFeature(); - applicationPartManager.PopulateFeature(feature); - Cache = new CompilerCache(fileProviderAccessor.FileProvider, feature.Views); - } - - /// - public ICompilerCache Cache { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs index 5729d0ddd3..09af95c9c1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs @@ -4,6 +4,7 @@ using System; using System.Linq.Expressions; using System.Reflection; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { @@ -13,17 +14,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider { - private readonly RazorCompiler _compiler; + private readonly IViewCompilerProvider _viewCompilerProvider; /// /// Initializes a new instance of . /// - /// The . - public DefaultRazorPageFactoryProvider(RazorCompiler compiler) + /// The . + public DefaultRazorPageFactoryProvider(IViewCompilerProvider viewCompilerProvider) { - _compiler = compiler; + _viewCompilerProvider = viewCompilerProvider; } + private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); + /// public RazorPageFactoryResult CreateFactory(string relativePath) { @@ -38,26 +41,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal relativePath = relativePath.Substring(1); } - var result = _compiler.Compile(relativePath); - if (result.Success) + var compileTask = Compiler.CompileAsync(relativePath); + var viewDescriptor = compileTask.GetAwaiter().GetResult(); + if (viewDescriptor.ViewAttribute != null) { - var compiledType = result.CompiledType; + var compiledType = viewDescriptor.ViewAttribute.ViewType; 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 propertyBindExpression = Expression.Bind(pathProperty, Expression.Constant(viewDescriptor.RelativePath)); var objectInitializeExpression = Expression.MemberInit(newExpression, propertyBindExpression); var pageFactory = Expression .Lambda>(objectInitializeExpression) .Compile(); - return new RazorPageFactoryResult(pageFactory, result.ExpirationTokens, result.IsPrecompiled); + return new RazorPageFactoryResult(pageFactory, viewDescriptor.ExpirationTokens, viewDescriptor.IsPrecompiled); } else { - return new RazorPageFactoryResult(result.ExpirationTokens); + return new RazorPageFactoryResult(viewDescriptor.ExpirationTokens); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs deleted file mode 100644 index 9e76c1835b..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs +++ /dev/null @@ -1,228 +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.Reflection; -using System.Runtime.Loader; -using System.Text; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - /// - /// A type that uses Roslyn to compile C# content. - /// - public class DefaultRoslynCompilationService : ICompilationService - { - // error CS0234: The type or namespace name 'C' does not exist in the namespace 'N' (are you missing - // an assembly reference?) - private const string CS0234 = nameof(CS0234); - // error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive - // or an assembly reference?) - private const string CS0246 = nameof(CS0246); - - private readonly CSharpCompiler _compiler; - private readonly ILogger _logger; - private readonly Action _compilationCallback; - - /// - /// Initalizes a new instance of the class. - /// - /// The . - /// Accessor to . - /// The . - public DefaultRoslynCompilationService( - CSharpCompiler compiler, - IOptions optionsAccessor, - ILoggerFactory loggerFactory) - { - _compiler = compiler; - _compilationCallback = optionsAccessor.Value.CompilationCallback; - _logger = loggerFactory.CreateLogger(); - } - - /// - public CompilationResult Compile(RazorCodeDocument codeDocument, RazorCSharpDocument cSharpDocument) - { - if (codeDocument == null) - { - throw new ArgumentNullException(nameof(codeDocument)); - } - - if (cSharpDocument == null) - { - throw new ArgumentNullException(nameof(codeDocument)); - } - - _logger.GeneratedCodeToAssemblyCompilationStart(codeDocument.Source.FileName); - - var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0; - - var assemblyName = Path.GetRandomFileName(); - var compilation = CreateCompilation(cSharpDocument.GeneratedCode, assemblyName); - - using (var assemblyStream = new MemoryStream()) - { - using (var pdbStream = new MemoryStream()) - { - var result = compilation.Emit( - assemblyStream, - pdbStream, - options: _compiler.EmitOptions); - - if (!result.Success) - { - return GetCompilationFailedResult( - codeDocument, - cSharpDocument.GeneratedCode, - assemblyName, - result.Diagnostics); - } - - assemblyStream.Seek(0, SeekOrigin.Begin); - pdbStream.Seek(0, SeekOrigin.Begin); - - var assembly = LoadAssembly(assemblyStream, pdbStream); - var type = assembly.GetExportedTypes().FirstOrDefault(a => !a.IsNested); - - _logger.GeneratedCodeToAssemblyCompilationEnd(codeDocument.Source.FileName, startTimestamp); - - return new CompilationResult(type); - } - } - } - - private CSharpCompilation CreateCompilation(string compilationContent, string assemblyName) - { - var sourceText = SourceText.From(compilationContent, Encoding.UTF8); - var syntaxTree = _compiler.CreateSyntaxTree(sourceText).WithFilePath(assemblyName); - var compilation = _compiler - .CreateCompilation(assemblyName) - .AddSyntaxTrees(syntaxTree); - compilation = ExpressionRewriter.Rewrite(compilation); - - var compilationContext = new RoslynCompilationContext(compilation); - _compilationCallback(compilationContext); - compilation = compilationContext.Compilation; - return compilation; - } - - // Internal for unit testing - internal CompilationResult GetCompilationFailedResult( - RazorCodeDocument codeDocument, - string compilationContent, - string assemblyName, - IEnumerable diagnostics) - { - var diagnosticGroups = diagnostics - .Where(IsError) - .GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal); - - var failures = new List(); - foreach (var group in diagnosticGroups) - { - var sourceFilePath = group.Key; - string sourceFileContent; - if (string.Equals(assemblyName, sourceFilePath, StringComparison.Ordinal)) - { - // The error is in the generated code and does not have a mapping line pragma - sourceFileContent = compilationContent; - sourceFilePath = Resources.GeneratedCodeFileName; - } - else - { - sourceFileContent = GetContent(codeDocument, sourceFilePath); - } - - string additionalMessage = null; - if (group.Any(g => - string.Equals(CS0234, g.Id, StringComparison.OrdinalIgnoreCase) || - string.Equals(CS0246, g.Id, StringComparison.OrdinalIgnoreCase))) - { - additionalMessage = Resources.FormatCompilation_DependencyContextIsNotSpecified( - "Microsoft.NET.Sdk.Web", - "PreserveCompilationContext"); - } - - var compilationFailure = new CompilationFailure( - sourceFilePath, - sourceFileContent, - compilationContent, - group.Select(GetDiagnosticMessage), - additionalMessage); - - failures.Add(compilationFailure); - } - - return new CompilationResult(failures); - } - - private static string GetFilePath(RazorCodeDocument codeDocument, Diagnostic diagnostic) - { - if (diagnostic.Location == Location.None) - { - return codeDocument.Source.FileName; - } - - return diagnostic.Location.GetMappedLineSpan().Path; - } - - private static bool IsError(Diagnostic diagnostic) - { - return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; - } - - public static Assembly LoadAssembly(MemoryStream assemblyStream, MemoryStream pdbStream) - { - var assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); - return assembly; - } - - private static string GetContent(RazorCodeDocument codeDocument, string filePath) - { - if (filePath == codeDocument.Source.FileName) - { - var chars = new char[codeDocument.Source.Length]; - codeDocument.Source.CopyTo(0, chars, 0, chars.Length); - return new string(chars); - } - - for (var i = 0; i < codeDocument.Imports.Count; i++) - { - var import = codeDocument.Imports[i]; - if (filePath == import.FileName) - { - var chars = new char[codeDocument.Source.Length]; - codeDocument.Source.CopyTo(0, chars, 0, chars.Length); - return new string(chars); - } - } - - return null; - } - - private static DiagnosticMessage GetDiagnosticMessage(Diagnostic diagnostic) - { - var mappedLineSpan = diagnostic.Location.GetMappedLineSpan(); - return new DiagnosticMessage( - diagnostic.GetMessage(), - CSharpDiagnosticFormatter.Instance.Format(diagnostic), - mappedLineSpan.Path, - mappedLineSpan.StartLinePosition.Line + 1, - mappedLineSpan.StartLinePosition.Character + 1, - mappedLineSpan.EndLinePosition.Line + 1, - mappedLineSpan.EndLinePosition.Character + 1); - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs deleted file mode 100644 index a5d42b2916..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCache.cs +++ /dev/null @@ -1,25 +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.AspNetCore.Mvc.Razor.Compilation; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - /// - /// Caches the result of runtime compilation of Razor files for the duration of the app lifetime. - /// - public interface ICompilerCache - { - /// - /// Get an existing compilation result, or create and add a new one if it is - /// not available in the cache or is expired. - /// - /// Application relative path to the file. - /// An delegate that will generate a compilation result. - /// A cached . - CompilerCacheResult GetOrAdd( - string relativePath, - Func compile); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCacheProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCacheProvider.cs deleted file mode 100644 index b621b0ac0b..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ICompilerCacheProvider.cs +++ /dev/null @@ -1,16 +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.Internal -{ - /// - /// Provides access to a cached instance. - /// - public interface ICompilerCacheProvider - { - /// - /// The cached instance. - /// - ICompilerCache Cache { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs deleted file mode 100644 index 413baaa174..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorCompiler.cs +++ /dev/null @@ -1,133 +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.Linq; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - public class RazorCompiler - { - private readonly ICompilationService _compilationService; - private readonly ICompilerCacheProvider _compilerCacheProvider; - private readonly RazorTemplateEngine _templateEngine; - private readonly Func _getCacheContext; - private readonly Func _getCompilationResultDelegate; - - public RazorCompiler( - ICompilationService compilationService, - ICompilerCacheProvider compilerCacheProvider, - RazorTemplateEngine 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/Internal/RazorViewCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs new file mode 100644 index 0000000000..eb4da99fd7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs @@ -0,0 +1,267 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + /// + /// Caches the result of runtime compilation of Razor files for the duration of the application lifetime. + /// + public class RazorViewCompiler : IViewCompiler + { + private readonly object _initializeLock = new object(); + private readonly object _cacheLock = new object(); + private readonly ConcurrentDictionary _normalizedPathLookup; + private readonly IFileProvider _fileProvider; + private readonly RazorTemplateEngine _templateEngine; + private readonly Action _compilationCallback; + private readonly ILogger _logger; + private readonly CSharpCompiler _csharpCompiler; + private IMemoryCache _cache; + + public RazorViewCompiler( + IFileProvider fileProvider, + RazorTemplateEngine templateEngine, + CSharpCompiler csharpCompiler, + Action compilationCallback, + IList precompiledViews, + ILogger logger) + { + if (fileProvider == null) + { + throw new ArgumentNullException(nameof(fileProvider)); + } + + if (templateEngine == null) + { + throw new ArgumentNullException(nameof(templateEngine)); + } + + if (csharpCompiler == null) + { + throw new ArgumentNullException(nameof(csharpCompiler)); + } + + if (compilationCallback == null) + { + throw new ArgumentNullException(nameof(compilationCallback)); + } + + if (precompiledViews == null) + { + throw new ArgumentNullException(nameof(precompiledViews)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _fileProvider = fileProvider; + _templateEngine = templateEngine; + _csharpCompiler = csharpCompiler; + _compilationCallback = compilationCallback; + _logger = logger; + + _normalizedPathLookup = new ConcurrentDictionary(StringComparer.Ordinal); + _cache = new MemoryCache(new MemoryCacheOptions()); + + foreach (var precompiledView in precompiledViews) + { + _cache.Set( + precompiledView.RelativePath, + Task.FromResult(precompiledView), + new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove }); + } + } + + /// + public Task CompileAsync(string relativePath) + { + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already + // normalized and a cache entry exists. + if (!_cache.TryGetValue(relativePath, out Task cachedResult)) + { + var normalizedPath = GetNormalizedPath(relativePath); + if (!_cache.TryGetValue(normalizedPath, out cachedResult)) + { + cachedResult = CreateCacheEntry(normalizedPath); + } + } + + return cachedResult; + } + + private Task CreateCacheEntry(string normalizedPath) + { + TaskCompletionSource compilationTaskSource = null; + MemoryCacheEntryOptions cacheEntryOptions; + Task cacheEntry; + + // Safe races cannot be allowed when compiling Razor pages. To ensure only one compilation request succeeds + // per file, we'll lock the creation of a cache entry. Creating the cache entry should be very quick. The + // actual work for compiling files happens outside the critical section. + lock (_cacheLock) + { + if (_cache.TryGetValue(normalizedPath, out cacheEntry)) + { + return cacheEntry; + } + + cacheEntryOptions = new MemoryCacheEntryOptions(); + + cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(normalizedPath)); + var projectItem = _templateEngine.Project.GetItem(normalizedPath); + if (!projectItem.Exists) + { + cacheEntry = Task.FromResult(new CompiledViewDescriptor + { + RelativePath = normalizedPath, + ExpirationTokens = cacheEntryOptions.ExpirationTokens, + }); + } + else + { + // A file exists and needs to be compiled. + compilationTaskSource = new TaskCompletionSource(); + foreach (var importItem in _templateEngine.GetImportItems(projectItem)) + { + cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(importItem.Path)); + } + cacheEntry = compilationTaskSource.Task; + } + + cacheEntry = _cache.Set(normalizedPath, cacheEntry, cacheEntryOptions); + } + + if (compilationTaskSource != null) + { + // Indicates that a file was found and needs to be compiled. + Debug.Assert(cacheEntryOptions != null); + + try + { + var descriptor = CompileAndEmit(normalizedPath); + descriptor.ExpirationTokens = cacheEntryOptions.ExpirationTokens; + compilationTaskSource.SetResult(descriptor); + } + catch (Exception ex) + { + compilationTaskSource.SetException(ex); + } + } + + return cacheEntry; + } + + protected virtual CompiledViewDescriptor CompileAndEmit(string relativePath) + { + var codeDocument = _templateEngine.CreateCodeDocument(relativePath); + var cSharpDocument = _templateEngine.GenerateCode(codeDocument); + + if (cSharpDocument.Diagnostics.Count > 0) + { + throw CompilationFailedExceptionFactory.Create( + codeDocument, + cSharpDocument.Diagnostics); + } + + var generatedAssembly = CompileAndEmit(codeDocument, cSharpDocument.GeneratedCode); + var exportedType = generatedAssembly.GetExportedTypes().FirstOrDefault(f => !f.IsNested); + return new CompiledViewDescriptor + { + ViewAttribute = new RazorViewAttribute(relativePath, exportedType), + RelativePath = relativePath, + }; + } + + internal Assembly CompileAndEmit(RazorCodeDocument codeDocument, string generatedCode) + { + _logger.GeneratedCodeToAssemblyCompilationStart(codeDocument.Source.FileName); + + var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0; + + var assemblyName = Path.GetRandomFileName(); + var compilation = CreateCompilation(generatedCode, assemblyName); + + using (var assemblyStream = new MemoryStream()) + using (var pdbStream = new MemoryStream()) + { + var result = compilation.Emit( + assemblyStream, + pdbStream, + options: _csharpCompiler.EmitOptions); + + if (!result.Success) + { + throw CompilationFailedExceptionFactory.Create( + codeDocument, + generatedCode, + assemblyName, + result.Diagnostics); + } + + assemblyStream.Seek(0, SeekOrigin.Begin); + pdbStream.Seek(0, SeekOrigin.Begin); + + var assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); + _logger.GeneratedCodeToAssemblyCompilationEnd(codeDocument.Source.FileName, startTimestamp); + + return assembly; + } + } + + private CSharpCompilation CreateCompilation(string compilationContent, string assemblyName) + { + var sourceText = SourceText.From(compilationContent, Encoding.UTF8); + var syntaxTree = _csharpCompiler.CreateSyntaxTree(sourceText).WithFilePath(assemblyName); + var compilation = _csharpCompiler + .CreateCompilation(assemblyName) + .AddSyntaxTrees(syntaxTree); + compilation = ExpressionRewriter.Rewrite(compilation); + + var compilationContext = new RoslynCompilationContext(compilation); + _compilationCallback(compilationContext); + compilation = compilationContext.Compilation; + return compilation; + } + + private string GetNormalizedPath(string relativePath) + { + Debug.Assert(relativePath != null); + if (relativePath.Length == 0) + { + return relativePath; + } + + if (!_normalizedPathLookup.TryGetValue(relativePath, out var normalizedPath)) + { + normalizedPath = ViewPath.NormalizePath(relativePath); + _normalizedPathLookup[relativePath] = normalizedPath; + } + + return normalizedPath; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompilerProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompilerProvider.cs new file mode 100644 index 0000000000..b435a9e186 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompilerProvider.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class RazorViewCompilerProvider : IViewCompilerProvider + { + private readonly RazorTemplateEngine _razorTemplateEngine; + private readonly ApplicationPartManager _applicationPartManager; + private readonly IRazorViewEngineFileProviderAccessor _fileProviderAccessor; + private readonly CSharpCompiler _csharpCompiler; + private readonly RazorViewEngineOptions _viewEngineOptions; + private readonly ILogger _logger; + private readonly Func _createCompiler; + + private object _initializeLock = new object(); + private bool _initialized; + private IViewCompiler _compiler; + + public RazorViewCompilerProvider( + ApplicationPartManager applicationPartManager, + RazorTemplateEngine razorTemplateEngine, + IRazorViewEngineFileProviderAccessor fileProviderAccessor, + CSharpCompiler csharpCompiler, + IOptions viewEngineOptionsAccessor, + ILoggerFactory loggerFactory) + { + _applicationPartManager = applicationPartManager; + _razorTemplateEngine = razorTemplateEngine; + _fileProviderAccessor = fileProviderAccessor; + _csharpCompiler = csharpCompiler; + _viewEngineOptions = viewEngineOptionsAccessor.Value; + + _logger = loggerFactory.CreateLogger(); + _createCompiler = CreateCompiler; + } + + public IViewCompiler GetCompiler() + { + var fileProvider = _fileProviderAccessor.FileProvider; + if (fileProvider is NullFileProvider) + { + var message = Resources.FormatFileProvidersAreRequired( + typeof(RazorViewEngineOptions).FullName, + nameof(RazorViewEngineOptions.FileProviders), + typeof(IFileProvider).FullName); + throw new InvalidOperationException(message); + } + + return LazyInitializer.EnsureInitialized( + ref _compiler, + ref _initialized, + ref _initializeLock, + _createCompiler); + } + + private IViewCompiler CreateCompiler() + { + var feature = new ViewsFeature(); + _applicationPartManager.PopulateFeature(feature); + + return new RazorViewCompiler( + _fileProviderAccessor.FileProvider, + _razorTemplateEngine, + _csharpCompiler, + _viewEngineOptions.CompilationCallback, + feature.ViewDescriptors, + _logger); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewPath.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewPath.cs new file mode 100644 index 0000000000..813493ee76 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewPath.cs @@ -0,0 +1,45 @@ +// 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.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public static class ViewPath + { + public static string NormalizePath(string path) + { + var addLeadingSlash = path[0] != '\\' && path[0] != '/'; + var transformSlashes = path.IndexOf('\\') != -1; + + if (!addLeadingSlash && !transformSlashes) + { + return path; + } + + var length = path.Length; + if (addLeadingSlash) + { + length++; + } + + var builder = new InplaceStringBuilder(length); + if (addLeadingSlash) + { + builder.Append('/'); + } + + for (var i = 0; i < path.Length; i++) + { + var ch = path[i]; + if (ch == '\\') + { + ch = '/'; + } + builder.Append(ch); + } + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs deleted file mode 100644 index b2efd6ce9b..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs +++ /dev/null @@ -1,12 +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; - -namespace Microsoft.AspNetCore.Mvc.ApplicationParts -{ - public class CompiledPageInfoFeature - { - public IList CompiledPages { get; } = new List(); - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs index 582790f1b8..03e0fcf22a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { @@ -30,24 +32,45 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// public void PopulateFeature(IEnumerable parts, ViewsFeature feature) { - foreach (var item in GetCompiledPageInfo(parts)) + foreach (var item in GetCompiledPageDescriptors(parts)) { - feature.Views.Add(item.Path, item.CompiledType); + feature.ViewDescriptors.Add(item); } } /// - /// Gets the sequence of from . + /// Gets the sequence of from . /// /// The s - /// The sequence of . - public static IEnumerable GetCompiledPageInfo(IEnumerable parts) + /// The sequence of . + public static IEnumerable GetCompiledPageDescriptors(IEnumerable parts) { - return parts.OfType() + var manifests = parts.OfType() .Select(part => CompiledViewManfiest.GetManifestType(part, FullyQualifiedManifestTypeName)) .Where(type => type != null) - .Select(type => (CompiledPageManifest)Activator.CreateInstance(type)) - .SelectMany(manifest => manifest.CompiledPages); + .Select(type => (CompiledPageManifest)Activator.CreateInstance(type)); + + foreach (var page in manifests.SelectMany(m => m.CompiledPages)) + { + var normalizedPath = ViewPath.NormalizePath(page.Path); + var modelType = page.CompiledType.GetProperty("Model")?.PropertyType; + + var pageAttribute = new RazorPageAttribute( + normalizedPath, + page.CompiledType, + modelType, + page.RoutePrefix); + + var viewDescriptor = new CompiledViewDescriptor + { + RelativePath = normalizedPath, + ViewAttribute = pageAttribute, + ExpirationTokens = Array.Empty(), + IsPrecompiled = true, + }; + + yield return viewDescriptor; + } } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAttribute.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAttribute.cs new file mode 100644 index 0000000000..8ec3a6ab57 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/RazorPageAttribute.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class RazorPageAttribute : RazorViewAttribute + { + public RazorPageAttribute(string path, Type viewType, Type modelType, string routeTemplate) + : base(path, viewType) + { + ModelType = modelType; + RouteTemplate = routeTemplate; + } + + public Type ModelType { get; } + + public string RouteTemplate { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs index d71909c43f..7684492ebd 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -56,17 +58,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } var cachedApplicationModels = new List(); - var pages = GetCompiledPages(); - foreach (var page in pages) + foreach (var pageDescriptor in GetCompiledPageDescriptors()) { - if (!page.Path.StartsWith(rootDirectory)) + var pageAttribute = (RazorPageAttribute)pageDescriptor.ViewAttribute; + + if (!pageDescriptor.RelativePath.StartsWith(rootDirectory)) { continue; } - var viewEnginePath = GetViewEnginePath(rootDirectory, page.Path); - var model = new PageApplicationModel(page.Path, viewEnginePath); - PageSelectorModel.PopulateDefaults(model, page.RoutePrefix); + var viewEnginePath = GetViewEnginePath(rootDirectory, pageDescriptor.RelativePath); + var model = new PageApplicationModel(pageDescriptor.RelativePath, viewEnginePath); + PageSelectorModel.PopulateDefaults(model, pageAttribute.RouteTemplate); cachedApplicationModels.Add(model); } @@ -75,8 +78,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } } - protected virtual IEnumerable GetCompiledPages() - => CompiledPageFeatureProvider.GetCompiledPageInfo(_applicationManager.ApplicationParts); + protected virtual IEnumerable GetCompiledPageDescriptors() + => CompiledPageFeatureProvider.GetCompiledPageDescriptors(_applicationManager.ApplicationParts); private string GetViewEnginePath(string rootDirectory, string path) { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs index d303a23e39..930d2ade69 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Reflection; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Internal; @@ -14,28 +14,42 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public class DefaultPageLoader : IPageLoader { private const string ModelPropertyName = "Model"; - - private readonly RazorCompiler _compiler; - public DefaultPageLoader(RazorCompiler compiler) + private readonly IViewCompilerProvider _viewCompilerProvider; + + public DefaultPageLoader(IViewCompilerProvider viewCompilerProvider) { - _compiler = compiler; + _viewCompilerProvider = viewCompilerProvider; } + private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); + public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor) { - var result = _compiler.Compile(actionDescriptor.RelativePath); - return CreateDescriptor(actionDescriptor, result.CompiledType.GetTypeInfo()); + var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); + var viewDescriptor = compileTask.GetAwaiter().GetResult(); + var viewAttribute = viewDescriptor.ViewAttribute; + + // Pages always have a model type. If it's not set explicitly by the developer using + // @model, it will be the same as the page type. + var modelType = viewAttribute.ViewType.GetProperty(ModelPropertyName)?.PropertyType; + + var pageAttribute = new RazorPageAttribute( + viewAttribute.Path, + viewAttribute.ViewType, + modelType, + routeTemplate: null); + + return CreateDescriptor(actionDescriptor, pageAttribute); } // Internal for unit testing - internal static CompiledPageActionDescriptor CreateDescriptor(PageActionDescriptor actionDescriptor, TypeInfo pageType) + internal static CompiledPageActionDescriptor CreateDescriptor( + PageActionDescriptor actionDescriptor, + RazorPageAttribute pageAttribute) { - // Pages always have a model type. If it's not set explicitly by the developer using - // @model, it will be the same as the page type. - // - // However, we allow it to be null here for ease of testing. - var modelType = pageType.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo(); + var pageType = pageAttribute.ViewType.GetTypeInfo(); + var modelType = pageAttribute.ModelType?.GetTypeInfo(); // Now we want to find the handler methods. If the model defines any handlers, then we'll use those, // otherwise look at the page itself (unless the page IS the model, in which case we already looked). diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/CompilationResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/CompilationResultTest.cs deleted file mode 100644 index 48bdbd201b..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/CompilationResultTest.cs +++ /dev/null @@ -1,32 +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.Linq; -using Microsoft.AspNetCore.Diagnostics; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor.Compilation -{ - public class CompilationResultTest - { - [Fact] - public void EnsureSuccessful_ThrowsIfCompilationFailed() - { - // Arrange - var compilationFailure = new CompilationFailure( - "test", - sourceFileContent: string.Empty, - compiledContent: string.Empty, - messages: Enumerable.Empty()); - var failures = new[] { compilationFailure }; - var result = new CompilationResult(failures); - - // Act and Assert - Assert.Null(result.CompiledType); - Assert.Same(failures, result.CompilationFailures); - var exception = Assert.Throws(() => result.EnsureSuccessful()); - var failure = Assert.Single(exception.CompilationFailures); - Assert.Same(compilationFailure, failure); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs index 24335abf08..aa3086edd7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation applicationPartManager.PopulateFeature(feature); // Assert - Assert.Empty(feature.Views); + Assert.Empty(feature.ViewDescriptors); } [Fact] @@ -52,21 +52,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation applicationPartManager.PopulateFeature(feature); // Assert - Assert.Collection(feature.Views.OrderBy(f => f.Key, StringComparer.Ordinal), + Assert.Collection(feature.ViewDescriptors.OrderBy(f => f.RelativePath, StringComparer.Ordinal), view => { - Assert.Equal("/Areas/Admin/Views/About.cshtml", view.Key); - Assert.Equal(typeof(int), view.Value); + Assert.Equal("/Areas/Admin/Views/About.cshtml", view.RelativePath); + Assert.Equal(typeof(int), view.ViewAttribute.ViewType); }, view => { - Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.Key); - Assert.Equal(typeof(string), view.Value); + Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.RelativePath); + Assert.Equal(typeof(string), view.ViewAttribute.ViewType); }, view => { - Assert.Equal("/Views/test/Index.cshtml", view.Key); - Assert.Equal(typeof(object), view.Value); + Assert.Equal("/Views/test/Index.cshtml", view.RelativePath); + Assert.Equal(typeof(object), view.ViewAttribute.ViewType); }); } @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation applicationPartManager.PopulateFeature(feature); // Assert - Assert.Empty(feature.Views); + Assert.Empty(feature.ViewDescriptors); } [Fact] @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation applicationPartManager.PopulateFeature(feature); // Assert - Assert.Empty(feature.Views); + Assert.Empty(feature.ViewDescriptors); } private class TestableViewsFeatureProvider : ViewsFeatureProvider diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs new file mode 100644 index 0000000000..a33723941c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs @@ -0,0 +1,43 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class CSharpCompilerTest + { + [Fact] + public void Compile_UsesApplicationsCompilationSettings_ForParsingAndCompilation() + { + // Arrange + var content = "public class Test {}"; + var define = "MY_CUSTOM_DEFINE"; + var options = new TestOptionsManager(); + options.Value.ParseOptions = options.Value.ParseOptions.WithPreprocessorSymbols(define); + var razorReferenceManager = new RazorReferenceManager(GetApplicationPartManager(), options); + var compiler = new CSharpCompiler(razorReferenceManager, options); + + // Act + var syntaxTree = compiler.CreateSyntaxTree(SourceText.From(content)); + + // Assert + Assert.Contains(define, syntaxTree.Options.PreprocessorSymbolNames); + } + + private static ApplicationPartManager GetApplicationPartManager() + { + var applicationPartManager = new ApplicationPartManager(); + var assembly = typeof(CSharpCompilerTest).GetTypeInfo().Assembly; + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); + + return applicationPartManager; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs deleted file mode 100644 index 9558e0b174..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs +++ /dev/null @@ -1,656 +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.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.Extensions.FileProviders; -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - public class CompilerCacheTest - { - 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 - { - get - { - var theoryData = new TheoryData(); - foreach (var path in _viewImportsPath) - { - theoryData.Add(path); - } - - return theoryData; - } - } - - [Fact] - public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem() - { - // Arrange - var item = new Mock(); - item - .SetupGet(i => i.Path) - .Returns("/some/path"); - item - .SetupGet(i => i.Exists) - .Returns(false); - - var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(fileProvider); - var compilerCacheContext = new CompilerCacheContext( - item.Object, - Enumerable.Empty(), - _ => throw new Exception("Shouldn't be called.")); - - // Act - var result = cache.GetOrAdd("/some/path", _ => compilerCacheContext); - - // Assert - Assert.False(result.Success); - } - - [Fact] - public void GetOrAdd_ReturnsCompilationResultFromFactory() - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var expected = new CompilationResult(typeof(TestView)); - - // Act - var result = cache.GetOrAdd(ViewPath, CreateContextFactory(expected)); - - // Assert - Assert.True(result.Success); - Assert.Equal(typeof(TestView), result.CompiledType); - Assert.Equal(ViewPath, result.RelativePath); - } - - [Theory] - [InlineData("/Areas/Finances/Views/Home/Index.cshtml")] - [InlineData(@"Areas\Finances\Views\Home\Index.cshtml")] - [InlineData(@"\Areas\Finances\Views\Home\Index.cshtml")] - [InlineData(@"\Areas\Finances\Views/Home\Index.cshtml")] - public void GetOrAdd_NormalizesPathSepartorForPaths(string relativePath) - { - // Arrange - var viewPath = "/Areas/Finances/Views/Home/Index.cshtml"; - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(viewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var expected = new CompilationResult(typeof(TestView)); - - // Act - 1 - var result1 = cache.GetOrAdd(@"Areas\Finances\Views\Home\Index.cshtml", CreateContextFactory(expected)); - - // Assert - 1 - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act - 2 - var result2 = cache.GetOrAdd(relativePath, ThrowsIfCalled); - - // Assert - 2 - Assert.Equal(typeof(TestView), result2.CompiledType); - } - - [Fact] - public void GetOrAdd_ReturnsFailedCompilationResult_IfFileWasRemovedFromFileSystem() - { - // Arrange - var fileProvider = new TestFileProvider(); - var fileInfo = fileProvider.AddFile(ViewPath, "some content"); - - var foundItem = new FileProviderRazorProjectItem(fileInfo, "", ViewPath); - - var notFoundItem = new Mock(); - notFoundItem - .SetupGet(i => i.Path) - .Returns(ViewPath); - notFoundItem - .SetupGet(i => i.Exists) - .Returns(false); - - var cache = new CompilerCache(fileProvider); - var expected = new CompilationResult(typeof(TestView)); - var cacheContext = new CompilerCacheContext(foundItem, Enumerable.Empty(), _ => expected); - - // Act 1 - var result1 = cache.GetOrAdd(ViewPath, _ => cacheContext); - - // Assert 1 - Assert.True(result1.Success); - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 2 - // Delete the file from the file system and set it's expiration token. - cacheContext = new CompilerCacheContext( - notFoundItem.Object, - Enumerable.Empty(), - _ => throw new Exception("Shouldn't be called.")); - fileProvider.GetChangeToken(ViewPath).HasChanged = true; - var result2 = cache.GetOrAdd(ViewPath, _ => cacheContext); - - // Assert 2 - Assert.False(result2.Success); - } - - [Fact] - public void GetOrAdd_ReturnsNewResultIfFileWasModified() - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var expected1 = new CompilationResult(typeof(TestView)); - var expected2 = new CompilationResult(typeof(DifferentView)); - - // Act 1 - var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected1)); - - // Assert 1 - Assert.True(result1.Success); - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 2 - // Verify we're getting cached results. - var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); - - // Assert 2 - Assert.True(result2.Success); - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 3 - fileProvider.GetChangeToken(ViewPath).HasChanged = true; - var result3 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected2)); - - // Assert 3 - Assert.True(result3.Success); - Assert.Equal(typeof(DifferentView), result3.CompiledType); - } - - [Theory] - [MemberData(nameof(ViewImportsPaths))] - public void GetOrAdd_ReturnsNewResult_IfAncestorViewImportsWereModified(string globalImportPath) - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var expected1 = new CompilationResult(typeof(TestView)); - var expected2 = new CompilationResult(typeof(DifferentView)); - - // Act 1 - var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected1)); - - // Assert 1 - Assert.True(result1.Success); - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 2 - // Verify we're getting cached results. - var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); - - // Assert 2 - Assert.True(result2.Success); - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 3 - fileProvider.GetChangeToken(globalImportPath).HasChanged = true; - var result3 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected2)); - - // Assert 2 - Assert.True(result3.Success); - Assert.Equal(typeof(DifferentView), result3.CompiledType); - } - - [Fact] - public void GetOrAdd_DoesNotQueryFileSystem_IfCachedFileTriggerWasNotSet() - { - // Arrange - var mockFileProvider = new Mock { CallBase = true }; - var fileProvider = mockFileProvider.Object; - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var expected = new CompilationResult(typeof(TestView)); - - // Act 1 - var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected)); - - // Assert 1 - Assert.True(result1.Success); - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 2 - var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); - - // Assert 2 - Assert.True(result2.Success); - } - - [Fact] - public void GetOrAdd_UsesViewsSpecifiedFromRazorFileInfoCollection() - { - // Arrange - var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(fileProvider, _precompiledViews); - - // Act - var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); - - // Assert - Assert.True(result.Success); - Assert.Equal(typeof(PreCompile), result.CompiledType); - Assert.Same(PrecompiledViewsPath, result.RelativePath); - } - - [Fact] - public void GetOrAdd_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledFile() - { - // Arrange - var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(fileProvider, _precompiledViews); - - // Act - fileProvider.Watch(PrecompiledViewsPath); - fileProvider.GetChangeToken(PrecompiledViewsPath).HasChanged = true; - var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); - - // Assert - Assert.True(result.Success); - Assert.True(result.IsPrecompiled); - Assert.Equal(typeof(PreCompile), result.CompiledType); - } - - [Theory] - [MemberData(nameof(ViewImportsPaths))] - public void GetOrAdd_DoesNotRecompile_IfFileTriggerWasSetForViewImports(string globalImportPath) - { - // Arrange - var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(fileProvider, _precompiledViews); - - // Act - fileProvider.Watch(globalImportPath); - fileProvider.GetChangeToken(globalImportPath).HasChanged = true; - var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); - - // Assert - Assert.True(result.Success); - Assert.Equal(typeof(PreCompile), result.CompiledType); - } - - [Fact] - public void GetOrAdd_ReturnsRuntimeCompiled() - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider, _precompiledViews); - var expected = new CompilationResult(typeof(TestView)); - - // Act 1 - var result1 = cache.GetOrAdd(ViewPath, CreateContextFactory(expected)); - - // Assert 1 - Assert.Equal(typeof(TestView), result1.CompiledType); - - // Act 2 - var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); - - // Assert 2 - Assert.True(result2.Success); - Assert.Equal(typeof(TestView), result2.CompiledType); - } - - [Fact] - public void GetOrAdd_ReturnsPrecompiledViews() - { - // Arrange - var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(fileProvider, _precompiledViews); - var expected = new CompilationResult(typeof(TestView)); - - // Act - var result1 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); - - // Assert - Assert.Equal(typeof(PreCompile), result1.CompiledType); - } - - [Theory] - [InlineData("/Areas/Finances/Views/Home/Index.cshtml")] - [InlineData(@"Areas\Finances\Views\Home\Index.cshtml")] - [InlineData(@"\Areas\Finances\Views\Home\Index.cshtml")] - [InlineData(@"\Areas\Finances\Views/Home\Index.cshtml")] - public void GetOrAdd_NormalizesPathSepartorForPathsThatArePrecompiled(string relativePath) - { - // Arrange - var expected = typeof(PreCompile); - var viewPath = "/Areas/Finances/Views/Home/Index.cshtml"; - var cache = new CompilerCache( - new TestFileProvider(), - new Dictionary - { - { viewPath, expected } - }); - - // Act - var result = cache.GetOrAdd(relativePath, ThrowsIfCalled); - - // Assert - Assert.Equal(typeof(PreCompile), result.CompiledType); - Assert.Equal(viewPath, result.RelativePath); - } - - [Theory] - [InlineData(@"Areas\Finances\Views\Home\Index.cshtml")] - [InlineData(@"\Areas\Finances\Views\Home\Index.cshtml")] - [InlineData(@"\Areas\Finances\Views/Home\Index.cshtml")] - public void ConstructorNormalizesPrecompiledViewPath(string viewPath) - { - // Arrange - var expected = typeof(PreCompile); - var cache = new CompilerCache( - new TestFileProvider(), - new Dictionary - { - { viewPath, expected } - }); - - // Act - var result = cache.GetOrAdd("/Areas/Finances/Views/Home/Index.cshtml", ThrowsIfCalled); - - // Assert - Assert.Equal(typeof(PreCompile), result.CompiledType); - } - - [Fact] - public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages() - { - // Arrange - var waitDuration = TimeSpan.FromSeconds(20); - var fileProvider = new TestFileProvider(); - fileProvider.AddFile("/Views/Home/Index.cshtml", "Index content"); - fileProvider.AddFile("/Views/Home/About.cshtml", "About content"); - var resetEvent1 = new AutoResetEvent(initialState: false); - var resetEvent2 = new ManualResetEvent(initialState: false); - var cache = new CompilerCache(fileProvider); - 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", path => - { - var projectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", path); - return new CompilerCacheContext(projectItem, Enumerable.Empty(), compile1); - }); - }); - - var task2 = Task.Run(() => - { - // Event 4 - return cache.GetOrAdd("/Views/Home/About.cshtml", path => - { - var projectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", path); - return new CompilerCacheContext(projectItem, Enumerable.Empty(), compile2); - }); - }); - - // Event 1 - resetEvent1.Set(); - - await Task.WhenAll(task1, task2); - - // Assert - var result1 = task1.Result; - var result2 = task2.Result; - Assert.True(compilingOne); - Assert.True(compilingTwo); - } - - [Fact] - public async Task GetOrAdd_DoesNotCreateMultipleCompilationResults_ForConcurrentInvocations() - { - // Arrange - var waitDuration = TimeSpan.FromSeconds(20); - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var resetEvent1 = new ManualResetEvent(initialState: false); - 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, path => - { - var projectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", path); - return new CompilerCacheContext(projectItem, Enumerable.Empty(), compile); - }); - }); - - var task2 = Task.Run(() => - { - // Event 4 - Assert.True(resetEvent2.WaitOne(waitDuration)); - return cache.GetOrAdd(ViewPath, ThrowsIfCalled); - }); - - // Event 1 - resetEvent1.Set(); - await Task.WhenAll(task1, task2); - - // Assert - var result1 = task1.Result; - var result2 = task2.Result; - Assert.Same(result1.CompiledType, result2.CompiledType); - } - - [Fact] - public void GetOrAdd_ThrowsIfNullFileProvider() - { - // Arrange - var expected = - $"'{typeof(RazorViewEngineOptions).FullName}.{nameof(RazorViewEngineOptions.FileProviders)}' must " + - $"not be empty. At least one '{typeof(IFileProvider).FullName}' is required to locate a view for " + - "rendering."; - var fileProvider = new NullFileProvider(); - var cache = new CompilerCache(fileProvider); - - // Act & Assert - var exception = Assert.Throws( - () => cache.GetOrAdd(ViewPath, _ => { throw new InvalidTimeZoneException(); })); - Assert.Equal(expected, exception.Message); - } - - [Fact] - public void GetOrAdd_CachesCompilationExceptions() - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var exception = new InvalidTimeZoneException(); - - // Act and Assert - 1 - var actual = Assert.Throws(() => - cache.GetOrAdd(ViewPath, _ => ThrowsIfCalled(ViewPath, exception))); - Assert.Same(exception, actual); - - // Act and Assert - 2 - actual = Assert.Throws(() => cache.GetOrAdd(ViewPath, ThrowsIfCalled)); - Assert.Same(exception, actual); - } - - [Fact] - public void GetOrAdd_ReturnsSuccessfulCompilationResultIfTriggerExpires() - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var changeToken = fileProvider.AddChangeToken(ViewPath); - var cache = new CompilerCache(fileProvider); - - // Act and Assert - 1 - Assert.Throws(() => - cache.GetOrAdd(ViewPath, _ => { throw new InvalidTimeZoneException(); })); - - // Act - 2 - changeToken.HasChanged = true; - var result = cache.GetOrAdd(ViewPath, CreateContextFactory(new CompilationResult(typeof(TestView)))); - - // Assert - 2 - Assert.Same(typeof(TestView), result.CompiledType); - } - - [Fact] - public void GetOrAdd_CachesExceptionsInCompilationResult() - { - // Arrange - var fileProvider = new TestFileProvider(); - fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(fileProvider); - var diagnosticMessages = new[] - { - 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, context)); - Assert.Same(compilationResult.CompilationFailures, ex.CompilationFailures); - - // Act and Assert - 2 - ex = Assert.Throws(() => cache.GetOrAdd(ViewPath, ThrowsIfCalled)); - Assert.Same(compilationResult.CompilationFailures, ex.CompilationFailures); - } - - private class TestView : RazorPage - { - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - private class PreCompile : RazorPage - { - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - public class DifferentView : RazorPage - { - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - private CompilerCacheContext ThrowsIfCalled(string path) => - ThrowsIfCalled(path, new Exception("Shouldn't be called")); - - private CompilerCacheContext ThrowsIfCalled(string path, Exception exception) - { - exception = exception ?? new Exception("Shouldn't be called"); - var projectItem = new FileProviderRazorProjectItem(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 FileProviderRazorProjectItem(new TestFileInfo(), "", path); - - var imports = new List(); - foreach (var importFilePath in _viewImportsPath) - { - var importProjectItem = new FileProviderRazorProjectItem(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/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerFailedExceptionFactoryTest.cs similarity index 67% rename from test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilerTest.cs rename to test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerFailedExceptionFactoryTest.cs index 2e068ff098..19afc340ce 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerFailedExceptionFactoryTest.cs @@ -3,15 +3,15 @@ using System.IO; using System.Text; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; -using Moq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { - public class RazorCompilerTest + public class CompilerFailedExceptionFactoryTest { [Fact] public void GetCompilationFailedResult_ReadsRazorErrorsFromPage() @@ -24,15 +24,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var razorProject = new FileProviderRazorProject(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); + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); // Assert var failure = Assert.Single(compilationResult.CompilationFailures); @@ -59,15 +55,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var razorProject = new FileProviderRazorProject(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); + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); // Assert var failure = Assert.Single(compilationResult.CompilationFailures); @@ -93,15 +85,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var razorProject = new FileProviderRazorProject(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); + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); // Assert var failure = Assert.Single(compilationResult.CompilationFailures); @@ -131,15 +119,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal 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); + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); // Assert Assert.Collection( @@ -183,13 +167,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal 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 FileProviderRazorProject(fileProvider))); // Act - var result = compiler.GetCompilationFailedResult(codeDocument, diagnostics); + var result = CompilationFailedExceptionFactory.Create(codeDocument, diagnostics); // Assert Assert.Collection(result.CompilationFailures, @@ -243,13 +223,85 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal }); } - private ICompilerCacheProvider GetCompilerCacheProvider(TestFileProvider fileProvider) + [Fact] + public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages() { - var compilerCache = new CompilerCache(fileProvider); - var compilerCacheProvider = new Mock(); - compilerCacheProvider.SetupGet(p => p.Cache).Returns(compilerCache); + // Arrange + var viewPath = "Views/Home/Index"; + var generatedCodeFileName = "Generated Code"; + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("view-content", viewPath)); + var assemblyName = "random-assembly-name"; - return compilerCacheProvider.Object; + var diagnostics = new[] + { + Diagnostic.Create( + GetRoslynDiagnostic("message-1"), + Location.Create( + viewPath, + new TextSpan(10, 5), + new LinePositionSpan(new LinePosition(10, 1), new LinePosition(10, 2)))), + Diagnostic.Create( + GetRoslynDiagnostic("message-2"), + Location.Create( + assemblyName, + new TextSpan(1, 6), + new LinePositionSpan(new LinePosition(1, 2), new LinePosition(3, 4)))), + Diagnostic.Create( + GetRoslynDiagnostic("message-3"), + Location.Create( + viewPath, + new TextSpan(40, 50), + new LinePositionSpan(new LinePosition(30, 5), new LinePosition(40, 12)))), + }; + + // Act + var compilationResult = CompilationFailedExceptionFactory.Create( + codeDocument, + "compilation-content", + assemblyName, + diagnostics); + + // Assert + Assert.Collection(compilationResult.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Equal("view-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-1", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(11, message.StartLine); + Assert.Equal(2, message.StartColumn); + Assert.Equal(11, message.EndLine); + Assert.Equal(3, message.EndColumn); + }, + message => + { + Assert.Equal("message-3", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(31, message.StartLine); + Assert.Equal(6, message.StartColumn); + Assert.Equal(41, message.EndLine); + Assert.Equal(13, message.EndColumn); + }); + }, + failure => + { + Assert.Equal(generatedCodeFileName, failure.SourceFilePath); + Assert.Equal("compilation-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-2", message.Message); + Assert.Equal(assemblyName, message.SourceFilePath); + Assert.Equal(2, message.StartLine); + Assert.Equal(3, message.StartColumn); + Assert.Equal(4, message.EndLine); + Assert.Equal(5, message.EndColumn); + }); + }); } private static RazorSourceDocument Create(string path, string template) @@ -265,5 +317,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal return RazorDiagnostic.Create(diagnosticDescriptor, sourceSpan); } + + private static DiagnosticDescriptor GetRoslynDiagnostic(string messageFormat) + { + return new DiagnosticDescriptor( + id: "someid", + title: "sometitle", + messageFormat: messageFormat, + category: "some-category", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs index c037571fc1..8a8697c731 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs @@ -4,8 +4,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -24,12 +22,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal Mock.Of(), Mock.Of(), }; - var compilerCache = new Mock(); + var descriptor = new CompiledViewDescriptor + { + RelativePath = path, + ExpirationTokens = expirationTokens, + }; + var compilerCache = new Mock(); compilerCache - .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) - .Returns(new CompilerCacheResult(path, expirationTokens)); + .Setup(f => f.CompileAsync(It.IsAny())) + .ReturnsAsync(descriptor); - var factoryProvider = new DefaultRazorPageFactoryProvider(CreateCompiler(compilerCache.Object)); + var factoryProvider = new DefaultRazorPageFactoryProvider(GetCompilerProvider(compilerCache.Object)); // Act var result = factoryProvider.CreateFactory(path); @@ -49,19 +52,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal Mock.Of(), Mock.Of(), }; - var compilerCache = new Mock(); + var descriptor = new CompiledViewDescriptor + { + RelativePath = relativePath, + ViewAttribute = new RazorViewAttribute(relativePath, typeof(TestRazorPage)), + ExpirationTokens = expirationTokens, + }; + var compilerCache = new Mock(); compilerCache - .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) - .Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), expirationTokens)); + .Setup(f => f.CompileAsync(It.IsAny())) + .ReturnsAsync(descriptor); - var factoryProvider = new DefaultRazorPageFactoryProvider(CreateCompiler(compilerCache.Object)); + var factoryProvider = new DefaultRazorPageFactoryProvider(GetCompilerProvider(compilerCache.Object)); // Act var result = factoryProvider.CreateFactory(relativePath); // Assert Assert.True(result.Success); - Assert.Equal(expirationTokens, result.ExpirationTokens); + Assert.Equal(expirationTokens, descriptor.ExpirationTokens); } [Fact] @@ -69,12 +78,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { // Arrange var relativePath = "/file-exists"; - var compilerCache = new Mock(); - compilerCache - .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) - .Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), new IChangeToken[0])); - - var factoryProvider = new DefaultRazorPageFactoryProvider(CreateCompiler(compilerCache.Object)); + var descriptor = new CompiledViewDescriptor + { + RelativePath = relativePath, + ViewAttribute = new RazorViewAttribute(relativePath, typeof(TestRazorPage)), + ExpirationTokens = Array.Empty(), + }; + var viewCompiler = new Mock(); + viewCompiler + .Setup(f => f.CompileAsync(It.IsAny())) + .ReturnsAsync(descriptor); + + var factoryProvider = new DefaultRazorPageFactoryProvider(GetCompilerProvider(viewCompiler.Object)); // Act var result = factoryProvider.CreateFactory(relativePath); @@ -85,17 +100,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal Assert.Equal("/file-exists", actual.Path); } - private RazorCompiler CreateCompiler(ICompilerCache cache) + private IViewCompilerProvider GetCompilerProvider(IViewCompiler cache) { - var compilerCacheProvider = new Mock(); + var compilerCacheProvider = new Mock(); compilerCacheProvider - .SetupGet(c => c.Cache) + .Setup(c => c.GetCompiler()) .Returns(cache); - return new RazorCompiler( - Mock.Of(), - compilerCacheProvider.Object, - new MvcRazorTemplateEngine(RazorEngine.Create(), new FileProviderRazorProject(new TestFileProvider()))); + return compilerCacheProvider.Object; } private class TestRazorPage : RazorPage diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs deleted file mode 100644 index b6280158cb..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs +++ /dev/null @@ -1,350 +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.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor.Internal -{ - public class DefaultRoslynCompilationServiceTest - { - [Fact] - public void Compile_SucceedsForCSharp7() - { - // Arrange - var content = @" -public class MyTestType -{ - private string _name; - - public string Name - { - get => _name; - set => _name = value ?? throw new System.ArgumentNullException(nameof(value)); - } -}"; - var compilationService = GetRoslynCompilationService(); - - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "test.cshtml")); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.Equal("MyTestType", result.CompiledType.Name); - Assert.Null(result.CompilationFailures); - } - - [Fact] - public void Compile_ReturnsCompilationResult() - { - // Arrange - var content = @" -public class MyTestType {}"; - - var compilationService = GetRoslynCompilationService(); - - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "test.cshtml")); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.Equal("MyTestType", result.CompiledType.Name); - } - - [Fact] - public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas() - { - // Arrange - var viewPath = "some-relative-path"; - var fileContent = "test file content"; - var content = $@" -#line 1 ""{viewPath}"" -this should fail"; - - var compilationService = GetRoslynCompilationService(); - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.IsType(result); - Assert.Null(result.CompiledType); - var compilationFailure = Assert.Single(result.CompilationFailures); - Assert.Equal(viewPath, compilationFailure.SourceFilePath); - Assert.Equal(fileContent, compilationFailure.SourceFileContent); - } - - [Fact] - public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable() - { - // Arrange - var viewPath = "some-relative-path"; - var fileContent = "file content"; - var content = "this should fail"; - - var compilationService = GetRoslynCompilationService(); - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.IsType(result); - Assert.Null(result.CompiledType); - - var compilationFailure = Assert.Single(result.CompilationFailures); - Assert.Equal("Generated Code", compilationFailure.SourceFilePath); - Assert.Equal(content, compilationFailure.SourceFileContent); - } - - [Fact] - public void Compile_UsesApplicationsCompilationSettings_ForParsingAndCompilation() - { - // Arrange - var viewPath = "some-relative-path"; - var content = @" -#if MY_CUSTOM_DEFINE -public class MyCustomDefinedClass {} -#else -public class MyNonCustomDefinedClass {} -#endif -"; - var options = GetOptions(); - options.ParseOptions = options.ParseOptions.WithPreprocessorSymbols("MY_CUSTOM_DEFINE"); - var compilationService = GetRoslynCompilationService(options: options); - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", viewPath)); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.NotNull(result.CompiledType); - Assert.Equal("MyCustomDefinedClass", result.CompiledType.Name); - } - - [Fact] - public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages() - { - // Arrange - var viewPath = "Views/Home/Index"; - var generatedCodeFileName = "Generated Code"; - var compilationService = GetRoslynCompilationService(); - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("view-content", viewPath)); - var assemblyName = "random-assembly-name"; - - var diagnostics = new[] - { - Diagnostic.Create( - GetDiagnosticDescriptor("message-1"), - Location.Create( - viewPath, - new TextSpan(10, 5), - new LinePositionSpan(new LinePosition(10, 1), new LinePosition(10, 2)))), - Diagnostic.Create( - GetDiagnosticDescriptor("message-2"), - Location.Create( - assemblyName, - new TextSpan(1, 6), - new LinePositionSpan(new LinePosition(1, 2), new LinePosition(3, 4)))), - Diagnostic.Create( - GetDiagnosticDescriptor("message-3"), - Location.Create( - viewPath, - new TextSpan(40, 50), - new LinePositionSpan(new LinePosition(30, 5), new LinePosition(40, 12)))), - }; - - // Act - var compilationResult = compilationService.GetCompilationFailedResult( - codeDocument, - "compilation-content", - assemblyName, - diagnostics); - - // Assert - Assert.Collection(compilationResult.CompilationFailures, - failure => - { - Assert.Equal(viewPath, failure.SourceFilePath); - Assert.Equal("view-content", failure.SourceFileContent); - Assert.Collection(failure.Messages, - message => - { - Assert.Equal("message-1", message.Message); - Assert.Equal(viewPath, message.SourceFilePath); - Assert.Equal(11, message.StartLine); - Assert.Equal(2, message.StartColumn); - Assert.Equal(11, message.EndLine); - Assert.Equal(3, message.EndColumn); - }, - message => - { - Assert.Equal("message-3", message.Message); - Assert.Equal(viewPath, message.SourceFilePath); - Assert.Equal(31, message.StartLine); - Assert.Equal(6, message.StartColumn); - Assert.Equal(41, message.EndLine); - Assert.Equal(13, message.EndColumn); - }); - }, - failure => - { - Assert.Equal(generatedCodeFileName, failure.SourceFilePath); - Assert.Equal("compilation-content", failure.SourceFileContent); - Assert.Collection(failure.Messages, - message => - { - Assert.Equal("message-2", message.Message); - Assert.Equal(assemblyName, message.SourceFilePath); - Assert.Equal(2, message.StartLine); - Assert.Equal(3, message.StartColumn); - Assert.Equal(4, message.EndLine); - Assert.Equal(5, message.EndColumn); - }); - }); - } - - [Fact] - public void Compile_RunsCallback() - { - // Arrange - var content = "public class MyTestType {}"; - RoslynCompilationContext usedCompilation = null; - var options = GetOptions(c => usedCompilation = c); - var compilationService = GetRoslynCompilationService(options: options); - - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path")); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - Assert.NotNull(usedCompilation); - Assert.Single(usedCompilation.Compilation.SyntaxTrees); - } - - [Fact] - public void Compile_DoesNotThrowIfReferencesWereClearedInCallback() - { - // Arrange - var options = GetOptions(context => - { - context.Compilation = context.Compilation.RemoveAllReferences(); - }); - var content = "public class MyTestType {}"; - var compilationService = GetRoslynCompilationService(options: options); - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.Single(result.CompilationFailures); - } - - [Fact] - public void Compile_SucceedsIfReferencesAreAddedInCallback() - { - // Arrange - var options = GetOptions(context => - { - var assemblyLocation = typeof(object).GetTypeInfo().Assembly.Location; - - context.Compilation = context - .Compilation - .AddReferences(MetadataReference.CreateFromFile(assemblyLocation)); - }); - var content = "public class MyTestType {}"; - var applicationPartManager = new ApplicationPartManager(); - var compilationService = GetRoslynCompilationService(applicationPartManager, options); - - var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); - - var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); - - // Act - var result = compilationService.Compile(codeDocument, csharpDocument); - - // Assert - Assert.Null(result.CompilationFailures); - Assert.NotNull(result.CompiledType); - } - - private static DiagnosticDescriptor GetDiagnosticDescriptor(string messageFormat) - { - return new DiagnosticDescriptor( - id: "someid", - title: "sometitle", - messageFormat: messageFormat, - category: "some-category", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - } - - private static RazorViewEngineOptions GetOptions(Action callback = null) - { - return new RazorViewEngineOptions - { - CompilationCallback = callback ?? (c => { }), - }; - } - - private static IOptions GetAccessor(RazorViewEngineOptions options) - { - var optionsAccessor = new Mock>(); - optionsAccessor.SetupGet(a => a.Value).Returns(options); - return optionsAccessor.Object; - } - - private static ApplicationPartManager GetApplicationPartManager() - { - var applicationPartManager = new ApplicationPartManager(); - var assembly = typeof(DefaultRoslynCompilationServiceTest).GetTypeInfo().Assembly; - applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); - applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); - - return applicationPartManager; - } - - private static DefaultRoslynCompilationService GetRoslynCompilationService( - ApplicationPartManager partManager = null, - RazorViewEngineOptions options = null) - { - partManager = partManager ?? GetApplicationPartManager(); - options = options ?? GetOptions(); - var optionsAccessor = GetAccessor(options); - var referenceManager = new RazorReferenceManager(partManager, optionsAccessor); - var compiler = new CSharpCompiler(referenceManager, optionsAccessor); - - return new DefaultRoslynCompilationService( - compiler, - optionsAccessor, - NullLoggerFactory.Instance); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerProviderTest.cs new file mode 100644 index 0000000000..c1007474f0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerProviderTest.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class RazorViewCompilerProviderTest + { + [Fact] + public void GetCompiler_ThrowsIfNullFileProvider() + { + // Arrange + var expected = + $"'{typeof(RazorViewEngineOptions).FullName}.{nameof(RazorViewEngineOptions.FileProviders)}' must " + + $"not be empty. At least one '{typeof(IFileProvider).FullName}' is required to locate a view for " + + "rendering."; + var fileProvider = new NullFileProvider(); + var accessor = new Mock(); + var applicationManager = new ApplicationPartManager(); + var options = new TestOptionsManager(); + var referenceManager = new RazorReferenceManager(applicationManager, options); + accessor.Setup(a => a.FileProvider).Returns(fileProvider); + var provider = new RazorViewCompilerProvider( + applicationManager, + new RazorTemplateEngine(RazorEngine.Create(), new FileProviderRazorProject(fileProvider)), + accessor.Object, + new CSharpCompiler(referenceManager, options), + options, + NullLoggerFactory.Instance); + + // Act & Assert + var exception = Assert.Throws( + () => provider.GetCompiler()); + Assert.Equal(expected, exception.Message); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs new file mode 100644 index 0000000000..17d3c8f139 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs @@ -0,0 +1,527 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class RazorViewCompilerTest + { + [Fact] + public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem() + { + // Arrange + var path = "/file/does-not-exist"; + var fileProvider = new TestFileProvider(); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act + var result1 = await viewCompiler.CompileAsync(path); + var result2 = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(result1, result2); + Assert.Null(result1.ViewAttribute); + Assert.Collection(result1.ExpirationTokens, + token => Assert.Equal(fileProvider.GetChangeToken(path), token)); + } + + [Fact] + public async Task CompileAsync_AddsChangeTokensForViewStartsIfFileExists() + { + // Arrange + var path = "/file/exists/FilePath.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "Content"); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.NotNull(result.ViewAttribute); + Assert.Collection(result.ExpirationTokens, + token => Assert.Same(fileProvider.GetChangeToken(path), token), + token => Assert.Same(fileProvider.GetChangeToken("/file/exists/_ViewImports.cshtml"), token), + token => Assert.Same(fileProvider.GetChangeToken("/file/_ViewImports.cshtml"), token), + token => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), token)); + } + + [Theory] + [InlineData("/Areas/Finances/Views/Home/Index.cshtml")] + [InlineData(@"Areas\Finances\Views\Home\Index.cshtml")] + [InlineData(@"\Areas\Finances\Views\Home\Index.cshtml")] + [InlineData(@"\Areas\Finances\Views/Home\Index.cshtml")] + public async Task CompileAsync_NormalizesPathSepartorForPaths(string relativePath) + { + // Arrange + var viewPath = "/Areas/Finances/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(viewPath, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act - 1 + var result1 = await viewCompiler.CompileAsync(@"Areas\Finances\Views\Home\Index.cshtml"); + + // Act - 2 + viewCompiler.Compile = _ => throw new Exception("Can't call me"); + var result2 = await viewCompiler.CompileAsync(relativePath); + + // Assert - 2 + Assert.Same(result1, result2); + } + + [Fact] + public async Task CompileAsync_InvalidatesCache_IfChangeTokenExpires() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act 1 + var result1 = await viewCompiler.CompileAsync(path); + + // Assert 1 + Assert.NotNull(result1.ViewAttribute); + + // Act 2 + fileProvider.DeleteFile(path); + fileProvider.GetChangeToken(path).HasChanged = true; + viewCompiler.Compile = _ => throw new Exception("Can't call me"); + var result2 = await viewCompiler.CompileAsync(path); + + // Assert 2 + Assert.NotSame(result1, result2); + Assert.Null(result2.ViewAttribute); + } + + [Fact] + public async Task CompileAsync_ReturnsNewResultIfFileWasModified() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + var expected2 = new CompiledViewDescriptor(); + + // Act 1 + var result1 = await viewCompiler.CompileAsync(path); + + // Assert 1 + Assert.NotNull(result1.ViewAttribute); + + // Act 2 + fileProvider.GetChangeToken(path).HasChanged = true; + viewCompiler.Compile = _ => expected2; + var result2 = await viewCompiler.CompileAsync(path); + + // Assert 2 + Assert.NotSame(result1, result2); + Assert.Same(expected2, result2); + } + + [Fact] + public async Task CompileAsync_ReturnsNewResult_IfAncestorViewImportsWereModified() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + var expected2 = new CompiledViewDescriptor(); + + // Act 1 + var result1 = await viewCompiler.CompileAsync(path); + + // Assert 1 + Assert.NotNull(result1.ViewAttribute); + + // Act 2 + fileProvider.GetChangeToken("/Views/_ViewImports.cshtml").HasChanged = true; + viewCompiler.Compile = _ => expected2; + var result2 = await viewCompiler.CompileAsync(path); + + // Assert 2 + Assert.NotSame(result1, result2); + Assert.Same(expected2, result2); + } + + [Fact] + public async Task CompileAsync_ReturnsPrecompiledViews() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(path, "some content"); + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + }; + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(precompiledView, result); + } + + [Fact] + public async Task CompileAsync_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledView() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(path, "some content"); + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + }; + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + fileProvider.Watch(path); + fileProvider.GetChangeToken(path).HasChanged = true; + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(precompiledView, result); + } + + + [Fact] + public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages() + { + // Arrange + var path1 = "/Views/Home/Index.cshtml"; + var path2 = "/Views/Home/About.cshtml"; + var waitDuration = TimeSpan.FromSeconds(20); + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path1, "Index content"); + fileProvider.AddFile(path2, "About content"); + var resetEvent1 = new AutoResetEvent(initialState: false); + var resetEvent2 = new ManualResetEvent(initialState: false); + var cache = GetViewCompiler(fileProvider); + var compilingOne = false; + var compilingTwo = false; + var result1 = new CompiledViewDescriptor(); + var result2 = new CompiledViewDescriptor(); + + cache.Compile = path => + { + if (path == path1) + { + compilingOne = true; + + // Event 2 + Assert.True(resetEvent1.WaitOne(waitDuration)); + + // Event 3 + Assert.True(resetEvent2.Set()); + + // Event 6 + Assert.True(resetEvent1.WaitOne(waitDuration)); + + Assert.True(compilingTwo); + + return result1; + } + else if (path == path2) + { + compilingTwo = true; + + // Event 4 + Assert.True(resetEvent2.WaitOne(waitDuration)); + + // Event 5 + Assert.True(resetEvent1.Set()); + + Assert.True(compilingOne); + + return result2; + } + else + { + throw new Exception(); + } + }; + + // Act + var task1 = Task.Run(() => cache.CompileAsync(path1)); + var task2 = Task.Run(() => cache.CompileAsync(path2)); + + // Event 1 + resetEvent1.Set(); + + await Task.WhenAll(task1, task2); + + // Assert + Assert.True(compilingOne); + Assert.True(compilingTwo); + Assert.Same(result1, task1.Result); + Assert.Same(result2, task2.Result); + } + + [Fact] + public async Task CompileAsync_DoesNotCreateMultipleCompilationResults_ForConcurrentInvocations() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var waitDuration = TimeSpan.FromSeconds(20); + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var resetEvent1 = new ManualResetEvent(initialState: false); + var resetEvent2 = new ManualResetEvent(initialState: false); + var compiler = GetViewCompiler(fileProvider); + + compiler.Compile = _ => + { + // Event 2 + resetEvent1.WaitOne(waitDuration); + + // Event 3 + resetEvent2.Set(); + return new CompiledViewDescriptor(); + }; + + // Act + var task1 = Task.Run(() => compiler.CompileAsync(path)); + var task2 = Task.Run(() => + { + // Event 4 + Assert.True(resetEvent2.WaitOne(waitDuration)); + return compiler.CompileAsync(path); + }); + + // Event 1 + resetEvent1.Set(); + await Task.WhenAll(task1, task2); + + // Assert + var result1 = task1.Result; + var result2 = task2.Result; + Assert.Same(result1, result2); + } + + [Fact] + public async Task GetOrAdd_CachesCompilationExceptions() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var exception = new InvalidTimeZoneException(); + var compiler = GetViewCompiler(fileProvider); + compiler.Compile = _ => throw exception; + + // Act and Assert - 1 + var actual = await Assert.ThrowsAsync( + () => compiler.CompileAsync(path)); + Assert.Same(exception, actual); + + // Act and Assert - 2 + compiler.Compile = _ => throw new Exception("Shouldn't be called"); + + actual = await Assert.ThrowsAsync( + () => compiler.CompileAsync(path)); + Assert.Same(exception, actual); + } + + [Fact] + public void Compile_SucceedsForCSharp7() + { + // Arrange + var content = @" +public class MyTestType +{ + private string _name; + + public string Name + { + get => _name; + set => _name = value ?? throw new System.ArgumentNullException(nameof(value)); + } +}"; + var compiler = GetViewCompiler(new TestFileProvider()); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("razor-content", "filename")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, content); + + // Assert + var exportedType = Assert.Single(result.ExportedTypes); + Assert.Equal("MyTestType", exportedType.Name); + } + + [Fact] + public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas() + { + // Arrange + var viewPath = "some-relative-path"; + var fileContent = "test file content"; + var content = $@" +#line 1 ""{viewPath}"" +this should fail"; + + var compiler = GetViewCompiler(new TestFileProvider()); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); + + // Act & Assert + var ex = Assert.Throws(() => compiler.CompileAndEmit(codeDocument, content)); + + var compilationFailure = Assert.Single(ex.CompilationFailures); + Assert.Equal(viewPath, compilationFailure.SourceFilePath); + Assert.Equal(fileContent, compilationFailure.SourceFileContent); + } + + [Fact] + public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable() + { + // Arrange + var viewPath = "some-relative-path"; + var fileContent = "file content"; + var content = "this should fail"; + + var compiler = GetViewCompiler(new TestFileProvider()); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); + + // Act & Assert + var ex = Assert.Throws(() => compiler.CompileAndEmit(codeDocument, content)); + + var compilationFailure = Assert.Single(ex.CompilationFailures); + Assert.Equal("Generated Code", compilationFailure.SourceFilePath); + Assert.Equal(content, compilationFailure.SourceFileContent); + } + + [Fact] + public void Compile_InvokessCallback() + { + // Arrange + var content = "public class MyTestType {}"; + var callbackInvoked = false; + var compiler = GetViewCompiler( + new TestFileProvider(), + context => + { + callbackInvoked = true; + }); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "some-path")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, content); + + // Assert + Assert.True(callbackInvoked); + } + + [Fact] + public void Compile_SucceedsIfReferencesAreAddedInCallback() + { + // Arrange + Action compilationCallback = context => + { + var assemblyLocation = typeof(object).Assembly.Location; + + context.Compilation = context + .Compilation + .AddReferences(MetadataReference.CreateFromFile(assemblyLocation)); + }; + + var applicationPartManager = new ApplicationPartManager(); + var referenceManager = new RazorReferenceManager( + applicationPartManager, + new TestOptionsManager()); + var compiler = GetViewCompiler( + compilationCallback: compilationCallback, + referenceManager: referenceManager); + + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, "public class Test {}"); + + // Assert + Assert.NotNull(result); + } + + private static TestRazorViewCompiler GetViewCompiler( + TestFileProvider fileProvider = null, + Action compilationCallback = null, + RazorReferenceManager referenceManager = null, + IList precompiledViews = null) + { + fileProvider = fileProvider ?? new TestFileProvider(); + compilationCallback = compilationCallback ?? (_ => { }); + var options = new TestOptionsManager(); + if (referenceManager == null) + { + var applicationPartManager = new ApplicationPartManager(); + var assembly = typeof(RazorViewCompilerTest).Assembly; + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); + + referenceManager = new RazorReferenceManager(applicationPartManager, options); + } + + precompiledViews = precompiledViews ?? Array.Empty(); + + var projectSystem = new FileProviderRazorProject(fileProvider); + var templateEngine = new RazorTemplateEngine(RazorEngine.Create(), projectSystem) + { + Options = + { + ImportsFileName = "_ViewImports.cshtml", + } + }; + var viewCompiler = new TestRazorViewCompiler( + fileProvider, + templateEngine, + new CSharpCompiler(referenceManager, options), + compilationCallback, + precompiledViews); + return viewCompiler; + } + + private class TestRazorViewCompiler : RazorViewCompiler + { + public TestRazorViewCompiler( + TestFileProvider fileProvider, + RazorTemplateEngine templateEngine, + CSharpCompiler csharpCompiler, + Action compilationCallback, + IList precompiledViews, + Func compile = null) : + base(fileProvider, templateEngine, csharpCompiler, compilationCallback, precompiledViews, NullLogger.Instance) + { + Compile = compile; + if (Compile == null) + { + Compile = path => new CompiledViewDescriptor + { + RelativePath = path, + ViewAttribute = new RazorViewAttribute(path, typeof(object)), + }; + } + } + + public Func Compile { get; set; } + + protected override CompiledViewDescriptor CompileAndEmit(string relativePath) + { + return Compile(relativePath); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs index 12fe6d81d6..30a3f9893c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs @@ -7,8 +7,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.CodeAnalysis; -using Microsoft.Extensions.Options; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal @@ -31,7 +29,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal var expectedReferenceDisplays = partReferences .Concat(new[] { objectAssemblyMetadataReference }) .Select(r => r.Display); - var referenceManager = new RazorReferenceManager(applicationPartManager, GetAccessor(options)); + var referenceManager = new RazorReferenceManager( + applicationPartManager, + new TestOptionsManager(options)); // Act var references = referenceManager.CompilationReferences; @@ -44,18 +44,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal private static ApplicationPartManager GetApplicationPartManager() { var applicationPartManager = new ApplicationPartManager(); - var assembly = typeof(DefaultRoslynCompilationServiceTest).GetTypeInfo().Assembly; + var assembly = typeof(ReferenceManagerTest).GetTypeInfo().Assembly; applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); return applicationPartManager; } - - private static IOptions GetAccessor(RazorViewEngineOptions options) - { - var optionsAccessor = new Mock>(); - optionsAccessor.SetupGet(a => a.Value).Returns(options); - return optionsAccessor.Object; - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ViewPathTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ViewPathTest.cs new file mode 100644 index 0000000000..e7055650e2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ViewPathTest.cs @@ -0,0 +1,36 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class ViewPathTest + { + [Theory] + [InlineData("/Views/Home/Index.cshtml")] + [InlineData("\\Views/Home/Index.cshtml")] + [InlineData("\\Views\\Home/Index.cshtml")] + [InlineData("\\Views\\Home\\Index.cshtml")] + public void NormalizePath_NormalizesSlashes(string input) + { + // Act + var normalizedPath = ViewPath.NormalizePath(input); + + // Assert + Assert.Equal("/Views/Home/Index.cshtml", normalizedPath); + } + + [Theory] + [InlineData("Views/Home/Index.cshtml")] + [InlineData("Views\\Home\\Index.cshtml")] + public void NormalizePath_AppendsLeadingSlash(string input) + { + // Act + var normalizedPath = ViewPath.NormalizePath(input); + + // Assert + Assert.Equal("/Views/Home/Index.cshtml", normalizedPath); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs index 62cda6fe5a..4119f572cd 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -15,12 +17,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnProvidersExecuting_AddsModelsForCompiledViews() { // Arrange - var info = new[] + var descriptors = new[] { - new CompiledPageInfo("/Pages/About.cshtml", typeof(object), routePrefix: string.Empty), - new CompiledPageInfo("/Pages/Home.cshtml", typeof(object), "some-prefix"), + GetDescriptor("/Pages/About.cshtml"), + GetDescriptor("/Pages/Home.cshtml", "some-prefix"), }; - var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions()); + var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions()); var context = new PageApplicationModelProviderContext(); // Act @@ -48,12 +50,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot() { // Arrange - var info = new[] + var descriptors = new[] { - new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty), - new CompiledPageInfo("/Pages/Admin/Index.cshtml", typeof(object), "some-template"), + GetDescriptor("/Pages/Index.cshtml"), + GetDescriptor("/Pages/Admin/Index.cshtml", "some-template"), }; - var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions { RootDirectory = "/" }); + var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions { RootDirectory = "/" }); var context = new PageApplicationModelProviderContext(); // Act @@ -83,12 +85,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage() { // Arrange - var info = new[] + var descriptors = new[] { - new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty), - new CompiledPageInfo("/Pages/Admin/Index.cshtml", typeof(object), "some-template"), + GetDescriptor("/Pages/Index.cshtml"), + GetDescriptor("/Pages/Admin/Index.cshtml", "some-template"), }; - var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions()); + var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions()); var context = new PageApplicationModelProviderContext(); // Act @@ -118,12 +120,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() { // Arrange - var info = new[] + var descriptors = new[] { - new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty), - new CompiledPageInfo("/Pages/Home.cshtml", typeof(object), "/some-prefix"), + GetDescriptor("/Pages/Index.cshtml"), + GetDescriptor("/Pages/Home.cshtml", "/some-prefix"), }; - var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions()); + var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions()); var context = new PageApplicationModelProviderContext(); // Act & Assert @@ -132,17 +134,27 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ex.Message); } + private static CompiledViewDescriptor GetDescriptor(string path, string prefix = "") + { + return new CompiledViewDescriptor + { + RelativePath = path, + ViewAttribute = new RazorPageAttribute(path, typeof(object), typeof(object), prefix), + }; + } + public class TestCompiledPageApplicationModelProvider : CompiledPageApplicationModelProvider { - private readonly IEnumerable _info; + private readonly IEnumerable _info; - public TestCompiledPageApplicationModelProvider(IEnumerable info, RazorPagesOptions options) + public TestCompiledPageApplicationModelProvider(IEnumerable info, RazorPagesOptions options) : base(new ApplicationPartManager(), new TestOptionsManager(options)) { _info = info; } - protected override IEnumerable GetCompiledPages() => _info; + + protected override IEnumerable GetCompiledPageDescriptors() => _info; } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs index 5b92030571..65e5755b8a 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Xunit; @@ -30,7 +31,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }; // Act - var actual = DefaultPageLoader.CreateDescriptor(expected, typeof(EmptyPage).GetTypeInfo()); + var actual = DefaultPageLoader.CreateDescriptor(expected, + new RazorPageAttribute(expected.RelativePath, typeof(EmptyPage), null, "")); // Assert Assert.Same(expected.ActionConstraints, actual.ActionConstraints); @@ -47,10 +49,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void CreateDescriptor_EmptyPage() { // Arrange - var type = typeof(EmptyPage).GetTypeInfo(); + var type = typeof(EmptyPage); // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), + new RazorPageAttribute("/Pages/Index", type, type, "")); // Assert Assert.Empty(result.BoundProperties); @@ -65,10 +68,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void CreateDescriptor_EmptyPageModel() { // Arrange - var type = typeof(EmptyPageWithPageModel).GetTypeInfo(); + var type = typeof(EmptyPageWithPageModel); // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), + new RazorPageAttribute("/Pages/Index", type, typeof(EmptyPageModel), "")); // Assert Assert.Empty(result.BoundProperties); @@ -130,10 +134,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void CreateDescriptor_FindsHandlerMethod_OnModel() { // Arrange - var type = typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(); + var type = typeof(PageWithHandlerThatGetsIgnored); // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), + new RazorPageAttribute("/Pages/Index", type, typeof(ModelWithHandler), "")); // Assert Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); @@ -166,10 +171,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void CreateDescriptor_FindsHandlerMethodOnPage_WhenModelHasNoHandlers() { // Arrange - var type = typeof(PageWithHandler).GetTypeInfo(); + var type = typeof(PageWithHandler); // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), + new RazorPageAttribute("/Pages/Index", type, typeof(PocoModel), "")); // Assert Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); @@ -581,7 +587,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public int IgnoreMe { get; set; } } - + [Fact] public void CreateBoundProperties_SupportsGet_OnClass() {