parent
c1dd95be2a
commit
452578e4a8
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue