Revisit the architecture of CompilerCache

Fixes #5912
This commit is contained in:
Pranav K 2017-05-16 11:07:58 -07:00
parent c1dd95be2a
commit 452578e4a8
40 changed files with 1590 additions and 2056 deletions

View File

@ -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
{
/// <summary>
/// Represents the result of compilation.
/// </summary>
public struct CompilationResult
{
/// <summary>
/// Initializes a new instance of <see cref="CompilationResult"/> for a successful compilation.
/// </summary>
/// <param name="type">The compiled type.</param>
public CompilationResult(Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
CompiledType = type;
CompilationFailures = null;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilationResult"/> for a failed compilation.
/// </summary>
/// <param name="compilationFailures"><see cref="CompilationFailure"/>s produced from parsing or
/// compiling the Razor file.</param>
public CompilationResult(IEnumerable<CompilationFailure> compilationFailures)
{
if (compilationFailures == null)
{
throw new ArgumentNullException(nameof(compilationFailures));
}
CompiledType = null;
CompilationFailures = compilationFailures;
}
/// <summary>
/// Gets the type produced as a result of compilation.
/// </summary>
/// <remarks>This property is <c>null</c> when compilation failed.</remarks>
public Type CompiledType { get; }
/// <summary>
/// Gets the <see cref="CompilationFailure"/>s produced from parsing or compiling the Razor file.
/// </summary>
/// <remarks>This property is <c>null</c> when compilation succeeded. An empty sequence
/// indicates a failed compilation.</remarks>
public IEnumerable<CompilationFailure> CompilationFailures { get; }
/// <summary>
/// Gets the <see cref="CompilationResult"/>.
/// </summary>
/// <returns>The current <see cref="CompilationResult"/> instance.</returns>
/// <exception cref="CompilationFailedException">Thrown if compilation failed.</exception>
public void EnsureSuccessful()
{
if (CompilationFailures != null)
{
throw new CompilationFailedException(CompilationFailures);
}
}
}
}

View File

@ -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; }
/// <summary>
/// Gets or sets the <see cref="RazorViewAttribute"/> decorating the sview.
/// </summary>
public RazorViewAttribute ViewAttribute { get; set; }
/// <summary>
/// <see cref="IChangeToken"/> instances that indicate when this result has expired.
/// </summary>
public IList<IChangeToken> ExpirationTokens { get; set; }
/// <summary>
/// Gets a value that determines if the view is precompiled.
/// </summary>
public bool IsPrecompiled { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Provides methods for compilation of a Razor page.
/// </summary>
public interface ICompilationService
{
/// <summary>
/// Compiles a <see cref="RazorCSharpDocument"/> and returns the result of compilation.
/// </summary>
/// <param name="codeDocument">
/// The <see cref="RazorCodeDocument"/> that contains the sources for the compilation.
/// </param>
/// <param name="cSharpDocument">
/// The <see cref="RazorCSharpDocument"/> to compile.
/// </param>
/// <returns>
/// A <see cref="CompilationResult"/> representing the result of compilation.
/// </returns>
CompilationResult Compile(RazorCodeDocument codeDocument, RazorCSharpDocument cSharpDocument);
}
}

View File

@ -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<CompiledViewDescriptor> CompileAsync(string relativePath);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
/// <summary>
/// Gets the path of the view.
/// </summary>
public string Path { get; }
/// <summary>
/// Gets the view type.
/// </summary>
public Type ViewType { get; }
}
}

View File

@ -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<string, Type> Views { get; } =
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
public IList<CompiledViewDescriptor> ViewDescriptors { get; } = new List<CompiledViewDescriptor>();
}
}

View File

@ -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<IChangeToken>(),
RelativePath = relativePath,
ViewAttribute = new RazorViewAttribute(relativePath, item.Type),
IsPrecompiled = true,
};
feature.ViewDescriptors.Add(viewDescriptor);
}
}
}

View File

@ -134,7 +134,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<CSharpCompiler>();
services.TryAddSingleton<RazorReferenceManager>();
// This caches compilation related details that are valid across the lifetime of the application.
services.TryAddSingleton<ICompilationService, DefaultRoslynCompilationService>();
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<MvcViewOptions>, MvcRazorMvcViewOptionsSetup>());
@ -153,9 +152,7 @@ namespace Microsoft.Extensions.DependencyInjection
DefaultRazorViewEngineFileProviderAccessor>();
services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>();
// Caches compilation artifacts across the lifetime of the application.
services.TryAddSingleton<ICompilerCacheProvider, DefaultCompilerCacheProvider>();
services.TryAddSingleton<IViewCompilerProvider, RazorViewCompilerProvider>();
// 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<RazorProject, FileProviderRazorProject>();
services.TryAddSingleton<RazorTemplateEngine, MvcRazorTemplateEngine>();
services.TryAddSingleton<RazorCompiler>();
services.TryAddSingleton<LazyMetadataReferenceFeature>();
services.TryAddSingleton<RazorEngine>(s =>

View File

@ -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<RazorDiagnostic> diagnostics)
{
// If a SourceLocation does not specify a file path, assume it is produced from parsing the current file.
var messageGroups = diagnostics.GroupBy(
razorError => razorError.Span.FilePath ?? codeDocument.Source.FileName,
StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
foreach (var group in messageGroups)
{
var filePath = group.Key;
var fileContent = ReadContent(codeDocument, filePath);
var compilationFailure = new CompilationFailure(
filePath,
fileContent,
compiledContent: string.Empty,
messages: group.Select(parserError => CreateDiagnosticMessage(parserError, filePath)));
failures.Add(compilationFailure);
}
return new CompilationFailedException(failures);
}
public static CompilationFailedException Create(
RazorCodeDocument codeDocument,
string compilationContent,
string assemblyName,
IEnumerable<Diagnostic> diagnostics)
{
var diagnosticGroups = diagnostics
.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
.GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
/// </summary>
public class CompilerCache : ICompilerCache
{
private readonly IFileProvider _fileProvider;
private readonly IMemoryCache _cache;
private readonly object _cacheLock = new object();
private readonly ConcurrentDictionary<string, string> _normalizedPathLookup =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
/// <summary>
/// Initializes a new instance of <see cref="CompilerCache"/>.
/// </summary>
/// <param name="fileProvider"><see cref="IFileProvider"/> used to locate Razor views.</param>
public CompilerCache(IFileProvider fileProvider)
{
if (fileProvider == null)
{
throw new ArgumentNullException(nameof(fileProvider));
}
_fileProvider = fileProvider;
_cache = new MemoryCache(new MemoryCacheOptions());
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
/// specified by <paramref name="views"/>.
/// </summary>
/// <param name="fileProvider"><see cref="IFileProvider"/> used to locate Razor views.</param>
/// <param name="views">A mapping of application relative paths of view to <see cref="Type"/>s that
/// have already been compiled.</param>
public CompilerCache(
IFileProvider fileProvider,
IDictionary<string, Type> 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));
}
}
/// <inheritdoc />
public CompilerCacheResult GetOrAdd(
string relativePath,
Func<string, CompilerCacheContext> cacheContextFactory)
{
if (relativePath == null)
{
throw new ArgumentNullException(nameof(relativePath));
}
if (cacheContextFactory == null)
{
throw new ArgumentNullException(nameof(cacheContextFactory));
}
Task<CompilerCacheResult> 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<CompilerCacheResult> CreateCacheEntry(
string normalizedPath,
Func<string, CompilerCacheContext> cacheContextFactory)
{
TaskCompletionSource<CompilerCacheResult> compilationTaskSource = null;
MemoryCacheEntryOptions cacheEntryOptions;
Task<CompilerCacheResult> cacheEntry;
CompilerCacheContext compilerCacheContext;
// Safe races cannot be allowed when compiling Razor pages. To ensure only one compilation request succeeds
// per file, we'll lock the creation of a cache entry. Creating the cache entry should be very quick. The
// 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<CompilerCacheResult>();
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;
}
}
}

View File

@ -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<RazorProjectItem> additionalCompilationItems,
Func<CompilerCacheContext, CompilationResult> compile)
{
ProjectItem = projectItem;
AdditionalCompilationItems = additionalCompilationItems;
Compile = compile;
}
public RazorProjectItem ProjectItem { get; }
public IEnumerable<RazorProjectItem> AdditionalCompilationItems { get; }
public Func<CompilerCacheContext, CompilationResult> Compile { get; }
}
}

View File

@ -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
{
/// <summary>
/// Result of <see cref="ICompilerCache"/>.
/// </summary>
public struct CompilerCacheResult
{
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="CompilationResult"/>.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="compilationResult">The <see cref="CompilationResult"/>.</param>
/// <param name="isPrecompiled"><c>true</c> if the view is precompiled, <c>false</c> otherwise.</param>
public CompilerCacheResult(string relativePath, CompilationResult compilationResult, bool isPrecompiled)
: this(relativePath, compilationResult, Array.Empty<IChangeToken>())
{
IsPrecompiled = isPrecompiled;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="CompilationResult"/>.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="compilationResult">The <see cref="CompilationResult"/>.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances that indicate when
/// this result has expired.</param>
public CompilerCacheResult(string relativePath, CompilationResult compilationResult, IList<IChangeToken> expirationTokens)
{
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
RelativePath = relativePath;
CompiledType = compilationResult.CompiledType;
ExpirationTokens = expirationTokens;
IsPrecompiled = false;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> for a file that could not be
/// found in the file system.
/// </summary>
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances that indicate when
/// this result has expired.</param>
public CompilerCacheResult(string relativePath, IList<IChangeToken> expirationTokens)
{
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
ExpirationTokens = expirationTokens;
RelativePath = null;
CompiledType = null;
IsPrecompiled = false;
}
/// <summary>
/// <see cref="IChangeToken"/> instances that indicate when this result has expired.
/// </summary>
public IList<IChangeToken> ExpirationTokens { get; }
/// <summary>
/// Gets a value that determines if the view was successfully found and compiled.
/// </summary>
public bool Success => CompiledType != null;
/// <summary>
/// Normalized relative path of the file.
/// </summary>
public string RelativePath { get; }
/// <summary>
/// The compiled <see cref="Type"/>.
/// </summary>
public Type CompiledType { get; }
/// <summary>
/// Gets a value that determines if the view is precompiled.
/// </summary>
public bool IsPrecompiled { get; }
}
}

View File

@ -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
{
/// <summary>
/// Default implementation for <see cref="ICompilerCacheProvider"/>.
/// </summary>
public class DefaultCompilerCacheProvider : ICompilerCacheProvider
{
/// <summary>
/// Initializes a new instance of <see cref="DefaultCompilerCacheProvider"/>.
/// </summary>
/// <param name="applicationPartManager">The <see cref="ApplicationPartManager" /></param>
/// <param name="fileProviderAccessor">The <see cref="IRazorViewEngineFileProviderAccessor"/>.</param>
public DefaultCompilerCacheProvider(
ApplicationPartManager applicationPartManager,
IRazorViewEngineFileProviderAccessor fileProviderAccessor)
{
var feature = new ViewsFeature();
applicationPartManager.PopulateFeature(feature);
Cache = new CompilerCache(fileProviderAccessor.FileProvider, feature.Views);
}
/// <inheritdoc />
public ICompilerCache Cache { get; }
}
}

View File

@ -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
/// </summary>
public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider
{
private readonly RazorCompiler _compiler;
private readonly IViewCompilerProvider _viewCompilerProvider;
/// <summary>
/// Initializes a new instance of <see cref="DefaultRazorPageFactoryProvider"/>.
/// </summary>
/// <param name="compiler">The <see cref="RazorCompiler"/>.</param>
public DefaultRazorPageFactoryProvider(RazorCompiler compiler)
/// <param name="viewCompilerProvider">The <see cref="IViewCompilerProvider"/>.</param>
public DefaultRazorPageFactoryProvider(IViewCompilerProvider viewCompilerProvider)
{
_compiler = compiler;
_viewCompilerProvider = viewCompilerProvider;
}
private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler();
/// <inheritdoc />
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<Func<IRazorPage>>(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);
}
}
}

View File

@ -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
{
/// <summary>
/// A type that uses Roslyn to compile C# content.
/// </summary>
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<RoslynCompilationContext> _compilationCallback;
/// <summary>
/// Initalizes a new instance of the <see cref="DefaultRoslynCompilationService"/> class.
/// </summary>
/// <param name="compiler">The <see cref="CSharpCompiler"/>.</param>
/// <param name="optionsAccessor">Accessor to <see cref="RazorViewEngineOptions"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public DefaultRoslynCompilationService(
CSharpCompiler compiler,
IOptions<RazorViewEngineOptions> optionsAccessor,
ILoggerFactory loggerFactory)
{
_compiler = compiler;
_compilationCallback = optionsAccessor.Value.CompilationCallback;
_logger = loggerFactory.CreateLogger<DefaultRoslynCompilationService>();
}
/// <inheritdoc />
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<Diagnostic> diagnostics)
{
var diagnosticGroups = diagnostics
.Where(IsError)
.GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
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);
}
}
}

View File

@ -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
{
/// <summary>
/// Caches the result of runtime compilation of Razor files for the duration of the app lifetime.
/// </summary>
public interface ICompilerCache
{
/// <summary>
/// Get an existing compilation result, or create and add a new one if it is
/// not available in the cache or is expired.
/// </summary>
/// <param name="relativePath">Application relative path to the file.</param>
/// <param name="compile">An delegate that will generate a compilation result.</param>
/// <returns>A cached <see cref="CompilationResult"/>.</returns>
CompilerCacheResult GetOrAdd(
string relativePath,
Func<string, CompilerCacheContext> compile);
}
}

View File

@ -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
{
/// <summary>
/// Provides access to a cached <see cref="ICompilerCache"/> instance.
/// </summary>
public interface ICompilerCacheProvider
{
/// <summary>
/// The cached <see cref="ICompilerCache"/> instance.
/// </summary>
ICompilerCache Cache { get; }
}
}

View File

@ -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<string, CompilerCacheContext> _getCacheContext;
private readonly Func<CompilerCacheContext, CompilationResult> _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<RazorDiagnostic> diagnostics)
{
// If a SourceLocation does not specify a file path, assume it is produced from parsing the current file.
var messageGroups = diagnostics.GroupBy(
razorError => razorError.Span.FilePath ?? codeDocument.Source.FileName,
StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
foreach (var group in messageGroups)
{
var filePath = group.Key;
var fileContent = ReadContent(codeDocument, filePath);
var compilationFailure = new CompilationFailure(
filePath,
fileContent,
compiledContent: string.Empty,
messages: group.Select(parserError => CreateDiagnosticMessage(parserError, filePath)));
failures.Add(compilationFailure);
}
return new CompilationResult(failures);
}
private static string ReadContent(RazorCodeDocument codeDocument, string filePath)
{
RazorSourceDocument sourceDocument = null;
if (string.IsNullOrEmpty(filePath) || string.Equals(codeDocument.Source.FileName, filePath, StringComparison.Ordinal))
{
sourceDocument = codeDocument.Source;
}
else
{
sourceDocument = codeDocument.Imports.FirstOrDefault(f => string.Equals(f.FileName, filePath, StringComparison.Ordinal));
}
if (sourceDocument != null)
{
var contentChars = new char[sourceDocument.Length];
sourceDocument.CopyTo(0, contentChars, 0, sourceDocument.Length);
return new string(contentChars);
}
return string.Empty;
}
private static DiagnosticMessage CreateDiagnosticMessage(
RazorDiagnostic razorDiagnostic,
string filePath)
{
var sourceSpan = razorDiagnostic.Span;
var message = razorDiagnostic.GetMessage();
return new DiagnosticMessage(
message: message,
formattedMessage: razorDiagnostic.ToString(),
filePath: filePath,
startLine: sourceSpan.LineIndex + 1,
startColumn: sourceSpan.CharacterIndex,
endLine: sourceSpan.LineIndex + 1,
endColumn: sourceSpan.CharacterIndex + sourceSpan.Length);
}
}
}

View File

@ -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
{
/// <summary>
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
/// </summary>
public class RazorViewCompiler : IViewCompiler
{
private readonly object _initializeLock = new object();
private readonly object _cacheLock = new object();
private readonly ConcurrentDictionary<string, string> _normalizedPathLookup;
private readonly IFileProvider _fileProvider;
private readonly RazorTemplateEngine _templateEngine;
private readonly Action<RoslynCompilationContext> _compilationCallback;
private readonly ILogger _logger;
private readonly CSharpCompiler _csharpCompiler;
private IMemoryCache _cache;
public RazorViewCompiler(
IFileProvider fileProvider,
RazorTemplateEngine templateEngine,
CSharpCompiler csharpCompiler,
Action<RoslynCompilationContext> compilationCallback,
IList<CompiledViewDescriptor> 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<string, string>(StringComparer.Ordinal);
_cache = new MemoryCache(new MemoryCacheOptions());
foreach (var precompiledView in precompiledViews)
{
_cache.Set(
precompiledView.RelativePath,
Task.FromResult(precompiledView),
new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove });
}
}
/// <inheritdoc />
public Task<CompiledViewDescriptor> 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<CompiledViewDescriptor> cachedResult))
{
var normalizedPath = GetNormalizedPath(relativePath);
if (!_cache.TryGetValue(normalizedPath, out cachedResult))
{
cachedResult = CreateCacheEntry(normalizedPath);
}
}
return cachedResult;
}
private Task<CompiledViewDescriptor> CreateCacheEntry(string normalizedPath)
{
TaskCompletionSource<CompiledViewDescriptor> compilationTaskSource = null;
MemoryCacheEntryOptions cacheEntryOptions;
Task<CompiledViewDescriptor> cacheEntry;
// Safe races cannot be allowed when compiling Razor pages. To ensure only one compilation request succeeds
// per file, we'll lock the creation of a cache entry. Creating the cache entry should be very quick. The
// actual work for compiling files happens outside the critical section.
lock (_cacheLock)
{
if (_cache.TryGetValue(normalizedPath, out cacheEntry))
{
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<CompiledViewDescriptor>();
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;
}
}
}

View File

@ -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<RazorViewCompiler> _logger;
private readonly Func<IViewCompiler> _createCompiler;
private object _initializeLock = new object();
private bool _initialized;
private IViewCompiler _compiler;
public RazorViewCompilerProvider(
ApplicationPartManager applicationPartManager,
RazorTemplateEngine razorTemplateEngine,
IRazorViewEngineFileProviderAccessor fileProviderAccessor,
CSharpCompiler csharpCompiler,
IOptions<RazorViewEngineOptions> viewEngineOptionsAccessor,
ILoggerFactory loggerFactory)
{
_applicationPartManager = applicationPartManager;
_razorTemplateEngine = razorTemplateEngine;
_fileProviderAccessor = fileProviderAccessor;
_csharpCompiler = csharpCompiler;
_viewEngineOptions = viewEngineOptionsAccessor.Value;
_logger = loggerFactory.CreateLogger<RazorViewCompiler>();
_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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<CompiledPageInfo> CompiledPages { get; } = new List<CompiledPageInfo>();
}
}

View File

@ -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
/// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> 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);
}
}
/// <summary>
/// Gets the sequence of <see cref="CompiledPageInfo"/> from <paramref name="parts"/>.
/// Gets the sequence of <see cref="CompiledViewDescriptor"/> from <paramref name="parts"/>.
/// </summary>
/// <param name="parts">The <see cref="ApplicationPart"/>s</param>
/// <returns>The sequence of <see cref="CompiledPageInfo"/>.</returns>
public static IEnumerable<CompiledPageInfo> GetCompiledPageInfo(IEnumerable<ApplicationPart> parts)
/// <returns>The sequence of <see cref="CompiledViewDescriptor"/>.</returns>
public static IEnumerable<CompiledViewDescriptor> GetCompiledPageDescriptors(IEnumerable<ApplicationPart> parts)
{
return parts.OfType<AssemblyPart>()
var manifests = parts.OfType<AssemblyPart>()
.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<IChangeToken>(),
IsPrecompiled = true,
};
yield return viewDescriptor;
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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<PageApplicationModel>();
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<CompiledPageInfo> GetCompiledPages()
=> CompiledPageFeatureProvider.GetCompiledPageInfo(_applicationManager.ApplicationParts);
protected virtual IEnumerable<CompiledViewDescriptor> GetCompiledPageDescriptors()
=> CompiledPageFeatureProvider.GetCompiledPageDescriptors(_applicationManager.ApplicationParts);
private string GetViewEnginePath(string rootDirectory, string path)
{

View File

@ -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).

View File

@ -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<AspNetCore.Diagnostics.DiagnosticMessage>());
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<CompilationFailedException>(() => result.EnsureSuccessful());
var failure = Assert.Single(exception.CompilationFailures);
Assert.Same(compilationFailure, failure);
}
}
}

View File

@ -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

View File

@ -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<RazorViewEngineOptions>();
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;
}
}
}

View File

@ -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<string, Type> _precompiledViews = new Dictionary<string, Type>
{
{ PrecompiledViewsPath, typeof(PreCompile) }
};
public static TheoryData ViewImportsPaths
{
get
{
var theoryData = new TheoryData<string>();
foreach (var path in _viewImportsPath)
{
theoryData.Add(path);
}
return theoryData;
}
}
[Fact]
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
{
// Arrange
var item = new Mock<RazorProjectItem>();
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<RazorProjectItem>(),
_ => 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<RazorProjectItem>();
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<RazorProjectItem>(), _ => 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<RazorProjectItem>(),
_ => 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<TestFileProvider> { 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<string, Type>
{
{ 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<string, Type>
{
{ 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<CompilerCacheContext, CompilationResult> compile1 = _ =>
{
compilingOne = true;
// Event 2
Assert.True(resetEvent1.WaitOne(waitDuration));
// Event 3
Assert.True(resetEvent2.Set());
// Event 6
Assert.True(resetEvent1.WaitOne(waitDuration));
Assert.True(compilingTwo);
return new CompilationResult(typeof(TestView));
};
Func<CompilerCacheContext, CompilationResult> compile2 = _ =>
{
compilingTwo = true;
// Event 4
Assert.True(resetEvent2.WaitOne(waitDuration));
// Event 5
Assert.True(resetEvent1.Set());
Assert.True(compilingOne);
return new CompilationResult(typeof(DifferentView));
};
// Act
var task1 = Task.Run(() =>
{
return cache.GetOrAdd("/Views/Home/Index.cshtml", path =>
{
var projectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", path);
return new CompilerCacheContext(projectItem, Enumerable.Empty<RazorProjectItem>(), 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<RazorProjectItem>(), 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<CompilerCacheContext, CompilationResult> compile = _ =>
{
// Event 2
resetEvent1.WaitOne(waitDuration);
// Event 3
resetEvent2.Set();
return new CompilationResult(typeof(TestView));
};
// Act
var task1 = Task.Run(() =>
{
return cache.GetOrAdd(ViewPath, path =>
{
var projectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", path);
return new CompilerCacheContext(projectItem, Enumerable.Empty<RazorProjectItem>(), 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<InvalidOperationException>(
() => 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<InvalidTimeZoneException>(() =>
cache.GetOrAdd(ViewPath, _ => ThrowsIfCalled(ViewPath, exception)));
Assert.Same(exception, actual);
// Act and Assert - 2
actual = Assert.Throws<InvalidTimeZoneException>(() => 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<InvalidTimeZoneException>(() =>
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<CompilationFailedException>(() => cache.GetOrAdd(ViewPath, context));
Assert.Same(compilationResult.CompilationFailures, ex.CompilationFailures);
// Act and Assert - 2
ex = Assert.Throws<CompilationFailedException>(() => 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<RazorProjectItem>(),
_ => throw exception);
}
private Func<string, CompilerCacheContext> CreateContextFactory(CompilationResult compile)
{
return path => CreateCacheContext(compile, path);
}
private CompilerCacheContext CreateCacheContext(CompilationResult compile, string path = ViewPath)
{
var projectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", path);
var imports = new List<RazorProjectItem>();
foreach (var importFilePath in _viewImportsPath)
{
var importProjectItem = new FileProviderRazorProjectItem(new TestFileInfo(), "", importFilePath);
imports.Add(importProjectItem);
}
return new CompilerCacheContext(projectItem, imports, _ => compile);
}
}
}

View File

@ -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<ICompilationService>(),
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<ICompilationService>(),
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<ICompilationService>(),
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<ICompilationService>(),
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<ICompilationService>(),
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<ICompilerCacheProvider>();
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);
}
}
}

View File

@ -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<IChangeToken>(),
Mock.Of<IChangeToken>(),
};
var compilerCache = new Mock<ICompilerCache>();
var descriptor = new CompiledViewDescriptor
{
RelativePath = path,
ExpirationTokens = expirationTokens,
};
var compilerCache = new Mock<IViewCompiler>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
.Returns(new CompilerCacheResult(path, expirationTokens));
.Setup(f => f.CompileAsync(It.IsAny<string>()))
.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<IChangeToken>(),
Mock.Of<IChangeToken>(),
};
var compilerCache = new Mock<ICompilerCache>();
var descriptor = new CompiledViewDescriptor
{
RelativePath = relativePath,
ViewAttribute = new RazorViewAttribute(relativePath, typeof(TestRazorPage)),
ExpirationTokens = expirationTokens,
};
var compilerCache = new Mock<IViewCompiler>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
.Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), expirationTokens));
.Setup(f => f.CompileAsync(It.IsAny<string>()))
.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<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
.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<IChangeToken>(),
};
var viewCompiler = new Mock<IViewCompiler>();
viewCompiler
.Setup(f => f.CompileAsync(It.IsAny<string>()))
.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<ICompilerCacheProvider>();
var compilerCacheProvider = new Mock<IViewCompilerProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Setup(c => c.GetCompiler())
.Returns(cache);
return new RazorCompiler(
Mock.Of<ICompilationService>(),
compilerCacheProvider.Object,
new MvcRazorTemplateEngine(RazorEngine.Create(), new FileProviderRazorProject(new TestFileProvider())));
return compilerCacheProvider.Object;
}
private class TestRazorPage : RazorPage

View File

@ -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<RazorDiagnostic>());
// 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<RazorDiagnostic>());
// 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<RazorDiagnostic>());
// Act
var result = compilationService.Compile(codeDocument, csharpDocument);
// Assert
Assert.IsType<CompilationResult>(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<RazorDiagnostic>());
// Act
var result = compilationService.Compile(codeDocument, csharpDocument);
// Assert
Assert.IsType<CompilationResult>(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<RazorDiagnostic>());
// 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<RazorDiagnostic>());
// 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<RazorDiagnostic>());
// 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<RazorDiagnostic>());
// 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<RoslynCompilationContext> callback = null)
{
return new RazorViewEngineOptions
{
CompilationCallback = callback ?? (c => { }),
};
}
private static IOptions<RazorViewEngineOptions> GetAccessor(RazorViewEngineOptions options)
{
var optionsAccessor = new Mock<IOptions<RazorViewEngineOptions>>();
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);
}
}
}

View File

@ -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<IRazorViewEngineFileProviderAccessor>();
var applicationManager = new ApplicationPartManager();
var options = new TestOptionsManager<RazorViewEngineOptions>();
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<InvalidOperationException>(
() => provider.GetCompiler());
Assert.Equal(expected, exception.Message);
}
}
}

View File

@ -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<InvalidTimeZoneException>(
() => compiler.CompileAsync(path));
Assert.Same(exception, actual);
// Act and Assert - 2
compiler.Compile = _ => throw new Exception("Shouldn't be called");
actual = await Assert.ThrowsAsync<InvalidTimeZoneException>(
() => 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<CompilationFailedException>(() => 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<CompilationFailedException>(() => 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<RoslynCompilationContext> 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<RazorViewEngineOptions>());
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<RoslynCompilationContext> compilationCallback = null,
RazorReferenceManager referenceManager = null,
IList<CompiledViewDescriptor> precompiledViews = null)
{
fileProvider = fileProvider ?? new TestFileProvider();
compilationCallback = compilationCallback ?? (_ => { });
var options = new TestOptionsManager<RazorViewEngineOptions>();
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<CompiledViewDescriptor>();
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<RoslynCompilationContext> compilationCallback,
IList<CompiledViewDescriptor> precompiledViews,
Func<string, CompiledViewDescriptor> 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<string, CompiledViewDescriptor> Compile { get; set; }
protected override CompiledViewDescriptor CompileAndEmit(string relativePath)
{
return Compile(relativePath);
}
}
}
}

View File

@ -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<RazorViewEngineOptions>(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<RazorViewEngineOptions> GetAccessor(RazorViewEngineOptions options)
{
var optionsAccessor = new Mock<IOptions<RazorViewEngineOptions>>();
optionsAccessor.SetupGet(a => a.Value).Returns(options);
return optionsAccessor.Object;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<CompiledPageInfo> _info;
private readonly IEnumerable<CompiledViewDescriptor> _info;
public TestCompiledPageApplicationModelProvider(IEnumerable<CompiledPageInfo> info, RazorPagesOptions options)
public TestCompiledPageApplicationModelProvider(IEnumerable<CompiledViewDescriptor> info, RazorPagesOptions options)
: base(new ApplicationPartManager(), new TestOptionsManager<RazorPagesOptions>(options))
{
_info = info;
}
protected override IEnumerable<CompiledPageInfo> GetCompiledPages() => _info;
protected override IEnumerable<CompiledViewDescriptor> GetCompiledPageDescriptors() => _info;
}
}
}

View File

@ -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()
{