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.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
{
|
{
|
||||||
public class ViewsFeature
|
public class ViewsFeature
|
||||||
{
|
{
|
||||||
public IDictionary<string, Type> Views { get; } =
|
public IList<CompiledViewDescriptor> ViewDescriptors { get; } = new List<CompiledViewDescriptor>();
|
||||||
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
{
|
{
|
||||||
|
|
@ -40,7 +42,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
|
|
||||||
foreach (var item in viewContainer.ViewInfos)
|
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<CSharpCompiler>();
|
||||||
services.TryAddSingleton<RazorReferenceManager>();
|
services.TryAddSingleton<RazorReferenceManager>();
|
||||||
// This caches compilation related details that are valid across the lifetime of the application.
|
// This caches compilation related details that are valid across the lifetime of the application.
|
||||||
services.TryAddSingleton<ICompilationService, DefaultRoslynCompilationService>();
|
|
||||||
|
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
ServiceDescriptor.Transient<IConfigureOptions<MvcViewOptions>, MvcRazorMvcViewOptionsSetup>());
|
ServiceDescriptor.Transient<IConfigureOptions<MvcViewOptions>, MvcRazorMvcViewOptionsSetup>());
|
||||||
|
|
@ -153,9 +152,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
DefaultRazorViewEngineFileProviderAccessor>();
|
DefaultRazorViewEngineFileProviderAccessor>();
|
||||||
|
|
||||||
services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>();
|
services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>();
|
||||||
|
services.TryAddSingleton<IViewCompilerProvider, RazorViewCompilerProvider>();
|
||||||
// Caches compilation artifacts across the lifetime of the application.
|
|
||||||
services.TryAddSingleton<ICompilerCacheProvider, DefaultCompilerCacheProvider>();
|
|
||||||
|
|
||||||
// In the default scenario the following services are singleton by virtue of being initialized as part of
|
// In the default scenario the following services are singleton by virtue of being initialized as part of
|
||||||
// creating the singleton RazorViewEngine instance.
|
// creating the singleton RazorViewEngine instance.
|
||||||
|
|
@ -166,7 +163,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<RazorProject, FileProviderRazorProject>();
|
services.TryAddSingleton<RazorProject, FileProviderRazorProject>();
|
||||||
services.TryAddSingleton<RazorTemplateEngine, MvcRazorTemplateEngine>();
|
services.TryAddSingleton<RazorTemplateEngine, MvcRazorTemplateEngine>();
|
||||||
services.TryAddSingleton<RazorCompiler>();
|
|
||||||
services.TryAddSingleton<LazyMetadataReferenceFeature>();
|
services.TryAddSingleton<LazyMetadataReferenceFeature>();
|
||||||
|
|
||||||
services.TryAddSingleton<RazorEngine>(s =>
|
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;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
{
|
{
|
||||||
|
|
@ -13,17 +14,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider
|
public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider
|
||||||
{
|
{
|
||||||
private readonly RazorCompiler _compiler;
|
private readonly IViewCompilerProvider _viewCompilerProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="DefaultRazorPageFactoryProvider"/>.
|
/// Initializes a new instance of <see cref="DefaultRazorPageFactoryProvider"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="compiler">The <see cref="RazorCompiler"/>.</param>
|
/// <param name="viewCompilerProvider">The <see cref="IViewCompilerProvider"/>.</param>
|
||||||
public DefaultRazorPageFactoryProvider(RazorCompiler compiler)
|
public DefaultRazorPageFactoryProvider(IViewCompilerProvider viewCompilerProvider)
|
||||||
{
|
{
|
||||||
_compiler = compiler;
|
_viewCompilerProvider = viewCompilerProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RazorPageFactoryResult CreateFactory(string relativePath)
|
public RazorPageFactoryResult CreateFactory(string relativePath)
|
||||||
{
|
{
|
||||||
|
|
@ -38,26 +41,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
relativePath = relativePath.Substring(1);
|
relativePath = relativePath.Substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = _compiler.Compile(relativePath);
|
var compileTask = Compiler.CompileAsync(relativePath);
|
||||||
if (result.Success)
|
var viewDescriptor = compileTask.GetAwaiter().GetResult();
|
||||||
|
if (viewDescriptor.ViewAttribute != null)
|
||||||
{
|
{
|
||||||
var compiledType = result.CompiledType;
|
var compiledType = viewDescriptor.ViewAttribute.ViewType;
|
||||||
|
|
||||||
var newExpression = Expression.New(compiledType);
|
var newExpression = Expression.New(compiledType);
|
||||||
var pathProperty = compiledType.GetTypeInfo().GetProperty(nameof(IRazorPage.Path));
|
var pathProperty = compiledType.GetTypeInfo().GetProperty(nameof(IRazorPage.Path));
|
||||||
|
|
||||||
// Generate: page.Path = relativePath;
|
// Generate: page.Path = relativePath;
|
||||||
// Use the normalized path specified from the result.
|
// 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 objectInitializeExpression = Expression.MemberInit(newExpression, propertyBindExpression);
|
||||||
var pageFactory = Expression
|
var pageFactory = Expression
|
||||||
.Lambda<Func<IRazorPage>>(objectInitializeExpression)
|
.Lambda<Func<IRazorPage>>(objectInitializeExpression)
|
||||||
.Compile();
|
.Compile();
|
||||||
return new RazorPageFactoryResult(pageFactory, result.ExpirationTokens, result.IsPrecompiled);
|
return new RazorPageFactoryResult(pageFactory, viewDescriptor.ExpirationTokens, viewDescriptor.IsPrecompiled);
|
||||||
}
|
}
|
||||||
else
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ApplicationParts
|
namespace Microsoft.AspNetCore.Mvc.ApplicationParts
|
||||||
{
|
{
|
||||||
|
|
@ -30,24 +32,45 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
|
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>
|
/// <summary>
|
||||||
/// Gets the sequence of <see cref="CompiledPageInfo"/> from <paramref name="parts"/>.
|
/// Gets the sequence of <see cref="CompiledViewDescriptor"/> from <paramref name="parts"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parts">The <see cref="ApplicationPart"/>s</param>
|
/// <param name="parts">The <see cref="ApplicationPart"/>s</param>
|
||||||
/// <returns>The sequence of <see cref="CompiledPageInfo"/>.</returns>
|
/// <returns>The sequence of <see cref="CompiledViewDescriptor"/>.</returns>
|
||||||
public static IEnumerable<CompiledPageInfo> GetCompiledPageInfo(IEnumerable<ApplicationPart> parts)
|
public static IEnumerable<CompiledViewDescriptor> GetCompiledPageDescriptors(IEnumerable<ApplicationPart> parts)
|
||||||
{
|
{
|
||||||
return parts.OfType<AssemblyPart>()
|
var manifests = parts.OfType<AssemblyPart>()
|
||||||
.Select(part => CompiledViewManfiest.GetManifestType(part, FullyQualifiedManifestTypeName))
|
.Select(part => CompiledViewManfiest.GetManifestType(part, FullyQualifiedManifestTypeName))
|
||||||
.Where(type => type != null)
|
.Where(type => type != null)
|
||||||
.Select(type => (CompiledPageManifest)Activator.CreateInstance(type))
|
.Select(type => (CompiledPageManifest)Activator.CreateInstance(type));
|
||||||
.SelectMany(manifest => manifest.CompiledPages);
|
|
||||||
|
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 System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
@ -56,17 +58,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
var cachedApplicationModels = new List<PageApplicationModel>();
|
var cachedApplicationModels = new List<PageApplicationModel>();
|
||||||
var pages = GetCompiledPages();
|
foreach (var pageDescriptor in GetCompiledPageDescriptors())
|
||||||
foreach (var page in pages)
|
|
||||||
{
|
{
|
||||||
if (!page.Path.StartsWith(rootDirectory))
|
var pageAttribute = (RazorPageAttribute)pageDescriptor.ViewAttribute;
|
||||||
|
|
||||||
|
if (!pageDescriptor.RelativePath.StartsWith(rootDirectory))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewEnginePath = GetViewEnginePath(rootDirectory, page.Path);
|
var viewEnginePath = GetViewEnginePath(rootDirectory, pageDescriptor.RelativePath);
|
||||||
var model = new PageApplicationModel(page.Path, viewEnginePath);
|
var model = new PageApplicationModel(pageDescriptor.RelativePath, viewEnginePath);
|
||||||
PageSelectorModel.PopulateDefaults(model, page.RoutePrefix);
|
PageSelectorModel.PopulateDefaults(model, pageAttribute.RouteTemplate);
|
||||||
|
|
||||||
cachedApplicationModels.Add(model);
|
cachedApplicationModels.Add(model);
|
||||||
}
|
}
|
||||||
|
|
@ -75,8 +78,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual IEnumerable<CompiledPageInfo> GetCompiledPages()
|
protected virtual IEnumerable<CompiledViewDescriptor> GetCompiledPageDescriptors()
|
||||||
=> CompiledPageFeatureProvider.GetCompiledPageInfo(_applicationManager.ApplicationParts);
|
=> CompiledPageFeatureProvider.GetCompiledPageDescriptors(_applicationManager.ApplicationParts);
|
||||||
|
|
||||||
private string GetViewEnginePath(string rootDirectory, string path)
|
private string GetViewEnginePath(string rootDirectory, string path)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
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.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
|
|
@ -14,28 +14,42 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public class DefaultPageLoader : IPageLoader
|
public class DefaultPageLoader : IPageLoader
|
||||||
{
|
{
|
||||||
private const string ModelPropertyName = "Model";
|
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)
|
public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor)
|
||||||
{
|
{
|
||||||
var result = _compiler.Compile(actionDescriptor.RelativePath);
|
var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath);
|
||||||
return CreateDescriptor(actionDescriptor, result.CompiledType.GetTypeInfo());
|
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 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
|
var pageType = pageAttribute.ViewType.GetTypeInfo();
|
||||||
// @model, it will be the same as the page type.
|
var modelType = pageAttribute.ModelType?.GetTypeInfo();
|
||||||
//
|
|
||||||
// However, we allow it to be null here for ease of testing.
|
|
||||||
var modelType = pageType.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo();
|
|
||||||
|
|
||||||
// Now we want to find the handler methods. If the model defines any handlers, then we'll use those,
|
// 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).
|
// 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);
|
applicationPartManager.PopulateFeature(feature);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(feature.Views);
|
Assert.Empty(feature.ViewDescriptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -52,21 +52,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
applicationPartManager.PopulateFeature(feature);
|
applicationPartManager.PopulateFeature(feature);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(feature.Views.OrderBy(f => f.Key, StringComparer.Ordinal),
|
Assert.Collection(feature.ViewDescriptors.OrderBy(f => f.RelativePath, StringComparer.Ordinal),
|
||||||
view =>
|
view =>
|
||||||
{
|
{
|
||||||
Assert.Equal("/Areas/Admin/Views/About.cshtml", view.Key);
|
Assert.Equal("/Areas/Admin/Views/About.cshtml", view.RelativePath);
|
||||||
Assert.Equal(typeof(int), view.Value);
|
Assert.Equal(typeof(int), view.ViewAttribute.ViewType);
|
||||||
},
|
},
|
||||||
view =>
|
view =>
|
||||||
{
|
{
|
||||||
Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.Key);
|
Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.RelativePath);
|
||||||
Assert.Equal(typeof(string), view.Value);
|
Assert.Equal(typeof(string), view.ViewAttribute.ViewType);
|
||||||
},
|
},
|
||||||
view =>
|
view =>
|
||||||
{
|
{
|
||||||
Assert.Equal("/Views/test/Index.cshtml", view.Key);
|
Assert.Equal("/Views/test/Index.cshtml", view.RelativePath);
|
||||||
Assert.Equal(typeof(object), view.Value);
|
Assert.Equal(typeof(object), view.ViewAttribute.ViewType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
applicationPartManager.PopulateFeature(feature);
|
applicationPartManager.PopulateFeature(feature);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(feature.Views);
|
Assert.Empty(feature.ViewDescriptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
applicationPartManager.PopulateFeature(feature);
|
applicationPartManager.PopulateFeature(feature);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(feature.Views);
|
Assert.Empty(feature.ViewDescriptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestableViewsFeatureProvider : ViewsFeatureProvider
|
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.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
using Moq;
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
{
|
{
|
||||||
public class RazorCompilerTest
|
public class CompilerFailedExceptionFactoryTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetCompilationFailedResult_ReadsRazorErrorsFromPage()
|
public void GetCompilationFailedResult_ReadsRazorErrorsFromPage()
|
||||||
|
|
@ -24,15 +24,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
var razorProject = new FileProviderRazorProject(fileProvider);
|
var razorProject = new FileProviderRazorProject(fileProvider);
|
||||||
|
|
||||||
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
|
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
|
||||||
var compiler = new RazorCompiler(
|
|
||||||
Mock.Of<ICompilationService>(),
|
|
||||||
GetCompilerCacheProvider(fileProvider),
|
|
||||||
templateEngine);
|
|
||||||
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
||||||
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
|
var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var failure = Assert.Single(compilationResult.CompilationFailures);
|
var failure = Assert.Single(compilationResult.CompilationFailures);
|
||||||
|
|
@ -59,15 +55,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
var razorProject = new FileProviderRazorProject(fileProvider);
|
var razorProject = new FileProviderRazorProject(fileProvider);
|
||||||
|
|
||||||
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
|
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
|
||||||
var compiler = new RazorCompiler(
|
|
||||||
Mock.Of<ICompilationService>(),
|
|
||||||
GetCompilerCacheProvider(fileProvider),
|
|
||||||
templateEngine);
|
|
||||||
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
||||||
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
|
var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var failure = Assert.Single(compilationResult.CompilationFailures);
|
var failure = Assert.Single(compilationResult.CompilationFailures);
|
||||||
|
|
@ -93,15 +85,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
var razorProject = new FileProviderRazorProject(fileProvider);
|
var razorProject = new FileProviderRazorProject(fileProvider);
|
||||||
|
|
||||||
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
|
var templateEngine = new MvcRazorTemplateEngine(razorEngine, razorProject);
|
||||||
var compiler = new RazorCompiler(
|
|
||||||
Mock.Of<ICompilationService>(),
|
|
||||||
GetCompilerCacheProvider(fileProvider),
|
|
||||||
templateEngine);
|
|
||||||
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
||||||
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
|
var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var failure = Assert.Single(compilationResult.CompilationFailures);
|
var failure = Assert.Single(compilationResult.CompilationFailures);
|
||||||
|
|
@ -131,15 +119,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
ImportsFileName = "_MyImports.cshtml",
|
ImportsFileName = "_MyImports.cshtml",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var compiler = new RazorCompiler(
|
|
||||||
Mock.Of<ICompilationService>(),
|
|
||||||
GetCompilerCacheProvider(fileProvider),
|
|
||||||
templateEngine);
|
|
||||||
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
var codeDocument = templateEngine.CreateCodeDocument(viewPath);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
var csharpDocument = templateEngine.GenerateCode(codeDocument);
|
||||||
var compilationResult = compiler.GetCompilationFailedResult(codeDocument, csharpDocument.Diagnostics);
|
var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -183,13 +167,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
GetRazorDiagnostic("message-4", new SourceLocation(viewImportsPath, 1, 3, 8), length: 4),
|
GetRazorDiagnostic("message-4", new SourceLocation(viewImportsPath, 1, 3, 8), length: 4),
|
||||||
};
|
};
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
var compiler = new RazorCompiler(
|
|
||||||
Mock.Of<ICompilationService>(),
|
|
||||||
GetCompilerCacheProvider(fileProvider),
|
|
||||||
new MvcRazorTemplateEngine(RazorEngine.Create(), new FileProviderRazorProject(fileProvider)));
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = compiler.GetCompilationFailedResult(codeDocument, diagnostics);
|
var result = CompilationFailedExceptionFactory.Create(codeDocument, diagnostics);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(result.CompilationFailures,
|
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);
|
// Arrange
|
||||||
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
|
var viewPath = "Views/Home/Index";
|
||||||
compilerCacheProvider.SetupGet(p => p.Cache).Returns(compilerCache);
|
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)
|
private static RazorSourceDocument Create(string path, string template)
|
||||||
|
|
@ -265,5 +317,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
|
|
||||||
return RazorDiagnostic.Create(diagnosticDescriptor, sourceSpan);
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -24,12 +22,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
Mock.Of<IChangeToken>(),
|
Mock.Of<IChangeToken>(),
|
||||||
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
|
compilerCache
|
||||||
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
|
.Setup(f => f.CompileAsync(It.IsAny<string>()))
|
||||||
.Returns(new CompilerCacheResult(path, expirationTokens));
|
.ReturnsAsync(descriptor);
|
||||||
|
|
||||||
var factoryProvider = new DefaultRazorPageFactoryProvider(CreateCompiler(compilerCache.Object));
|
var factoryProvider = new DefaultRazorPageFactoryProvider(GetCompilerProvider(compilerCache.Object));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factoryProvider.CreateFactory(path);
|
var result = factoryProvider.CreateFactory(path);
|
||||||
|
|
@ -49,19 +52,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
Mock.Of<IChangeToken>(),
|
Mock.Of<IChangeToken>(),
|
||||||
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
|
compilerCache
|
||||||
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
|
.Setup(f => f.CompileAsync(It.IsAny<string>()))
|
||||||
.Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), expirationTokens));
|
.ReturnsAsync(descriptor);
|
||||||
|
|
||||||
var factoryProvider = new DefaultRazorPageFactoryProvider(CreateCompiler(compilerCache.Object));
|
var factoryProvider = new DefaultRazorPageFactoryProvider(GetCompilerProvider(compilerCache.Object));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factoryProvider.CreateFactory(relativePath);
|
var result = factoryProvider.CreateFactory(relativePath);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.Success);
|
Assert.True(result.Success);
|
||||||
Assert.Equal(expirationTokens, result.ExpirationTokens);
|
Assert.Equal(expirationTokens, descriptor.ExpirationTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -69,12 +78,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var relativePath = "/file-exists";
|
var relativePath = "/file-exists";
|
||||||
var compilerCache = new Mock<ICompilerCache>();
|
var descriptor = new CompiledViewDescriptor
|
||||||
compilerCache
|
{
|
||||||
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<string, CompilerCacheContext>>()))
|
RelativePath = relativePath,
|
||||||
.Returns(new CompilerCacheResult(relativePath, new CompilationResult(typeof(TestRazorPage)), new IChangeToken[0]));
|
ViewAttribute = new RazorViewAttribute(relativePath, typeof(TestRazorPage)),
|
||||||
|
ExpirationTokens = Array.Empty<IChangeToken>(),
|
||||||
var factoryProvider = new DefaultRazorPageFactoryProvider(CreateCompiler(compilerCache.Object));
|
};
|
||||||
|
var viewCompiler = new Mock<IViewCompiler>();
|
||||||
|
viewCompiler
|
||||||
|
.Setup(f => f.CompileAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(descriptor);
|
||||||
|
|
||||||
|
var factoryProvider = new DefaultRazorPageFactoryProvider(GetCompilerProvider(viewCompiler.Object));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = factoryProvider.CreateFactory(relativePath);
|
var result = factoryProvider.CreateFactory(relativePath);
|
||||||
|
|
@ -85,17 +100,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
Assert.Equal("/file-exists", actual.Path);
|
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
|
compilerCacheProvider
|
||||||
.SetupGet(c => c.Cache)
|
.Setup(c => c.GetCompiler())
|
||||||
.Returns(cache);
|
.Returns(cache);
|
||||||
|
|
||||||
return new RazorCompiler(
|
return compilerCacheProvider.Object;
|
||||||
Mock.Of<ICompilationService>(),
|
|
||||||
compilerCacheProvider.Object,
|
|
||||||
new MvcRazorTemplateEngine(RazorEngine.Create(), new FileProviderRazorProject(new TestFileProvider())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestRazorPage : RazorPage
|
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.Compilation;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
|
|
@ -31,7 +29,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
var expectedReferenceDisplays = partReferences
|
var expectedReferenceDisplays = partReferences
|
||||||
.Concat(new[] { objectAssemblyMetadataReference })
|
.Concat(new[] { objectAssemblyMetadataReference })
|
||||||
.Select(r => r.Display);
|
.Select(r => r.Display);
|
||||||
var referenceManager = new RazorReferenceManager(applicationPartManager, GetAccessor(options));
|
var referenceManager = new RazorReferenceManager(
|
||||||
|
applicationPartManager,
|
||||||
|
new TestOptionsManager<RazorViewEngineOptions>(options));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var references = referenceManager.CompilationReferences;
|
var references = referenceManager.CompilationReferences;
|
||||||
|
|
@ -44,18 +44,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
private static ApplicationPartManager GetApplicationPartManager()
|
private static ApplicationPartManager GetApplicationPartManager()
|
||||||
{
|
{
|
||||||
var applicationPartManager = new ApplicationPartManager();
|
var applicationPartManager = new ApplicationPartManager();
|
||||||
var assembly = typeof(DefaultRoslynCompilationServiceTest).GetTypeInfo().Assembly;
|
var assembly = typeof(ReferenceManagerTest).GetTypeInfo().Assembly;
|
||||||
applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly));
|
applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly));
|
||||||
applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider());
|
applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider());
|
||||||
|
|
||||||
return applicationPartManager;
|
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 System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
@ -15,12 +17,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void OnProvidersExecuting_AddsModelsForCompiledViews()
|
public void OnProvidersExecuting_AddsModelsForCompiledViews()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var info = new[]
|
var descriptors = new[]
|
||||||
{
|
{
|
||||||
new CompiledPageInfo("/Pages/About.cshtml", typeof(object), routePrefix: string.Empty),
|
GetDescriptor("/Pages/About.cshtml"),
|
||||||
new CompiledPageInfo("/Pages/Home.cshtml", typeof(object), "some-prefix"),
|
GetDescriptor("/Pages/Home.cshtml", "some-prefix"),
|
||||||
};
|
};
|
||||||
var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions());
|
var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions());
|
||||||
var context = new PageApplicationModelProviderContext();
|
var context = new PageApplicationModelProviderContext();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -48,12 +50,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot()
|
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var info = new[]
|
var descriptors = new[]
|
||||||
{
|
{
|
||||||
new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty),
|
GetDescriptor("/Pages/Index.cshtml"),
|
||||||
new CompiledPageInfo("/Pages/Admin/Index.cshtml", typeof(object), "some-template"),
|
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();
|
var context = new PageApplicationModelProviderContext();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -83,12 +85,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage()
|
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var info = new[]
|
var descriptors = new[]
|
||||||
{
|
{
|
||||||
new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty),
|
GetDescriptor("/Pages/Index.cshtml"),
|
||||||
new CompiledPageInfo("/Pages/Admin/Index.cshtml", typeof(object), "some-template"),
|
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();
|
var context = new PageApplicationModelProviderContext();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -118,12 +120,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern()
|
public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var info = new[]
|
var descriptors = new[]
|
||||||
{
|
{
|
||||||
new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty),
|
GetDescriptor("/Pages/Index.cshtml"),
|
||||||
new CompiledPageInfo("/Pages/Home.cshtml", typeof(object), "/some-prefix"),
|
GetDescriptor("/Pages/Home.cshtml", "/some-prefix"),
|
||||||
};
|
};
|
||||||
var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions());
|
var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions());
|
||||||
var context = new PageApplicationModelProviderContext();
|
var context = new PageApplicationModelProviderContext();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
|
|
@ -132,17 +134,27 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
ex.Message);
|
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
|
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))
|
: base(new ApplicationPartManager(), new TestOptionsManager<RazorPagesOptions>(options))
|
||||||
{
|
{
|
||||||
_info = info;
|
_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 System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -30,7 +31,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var actual = DefaultPageLoader.CreateDescriptor(expected, typeof(EmptyPage).GetTypeInfo());
|
var actual = DefaultPageLoader.CreateDescriptor(expected,
|
||||||
|
new RazorPageAttribute(expected.RelativePath, typeof(EmptyPage), null, ""));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(expected.ActionConstraints, actual.ActionConstraints);
|
Assert.Same(expected.ActionConstraints, actual.ActionConstraints);
|
||||||
|
|
@ -47,10 +49,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void CreateDescriptor_EmptyPage()
|
public void CreateDescriptor_EmptyPage()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var type = typeof(EmptyPage).GetTypeInfo();
|
var type = typeof(EmptyPage);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type);
|
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(),
|
||||||
|
new RazorPageAttribute("/Pages/Index", type, type, ""));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result.BoundProperties);
|
Assert.Empty(result.BoundProperties);
|
||||||
|
|
@ -65,10 +68,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void CreateDescriptor_EmptyPageModel()
|
public void CreateDescriptor_EmptyPageModel()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var type = typeof(EmptyPageWithPageModel).GetTypeInfo();
|
var type = typeof(EmptyPageWithPageModel);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type);
|
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(),
|
||||||
|
new RazorPageAttribute("/Pages/Index", type, typeof(EmptyPageModel), ""));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result.BoundProperties);
|
Assert.Empty(result.BoundProperties);
|
||||||
|
|
@ -130,10 +134,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public void CreateDescriptor_FindsHandlerMethod_OnModel()
|
public void CreateDescriptor_FindsHandlerMethod_OnModel()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var type = typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo();
|
var type = typeof(PageWithHandlerThatGetsIgnored);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type);
|
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(),
|
||||||
|
new RazorPageAttribute("/Pages/Index", type, typeof(ModelWithHandler), ""));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name));
|
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()
|
public void CreateDescriptor_FindsHandlerMethodOnPage_WhenModelHasNoHandlers()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var type = typeof(PageWithHandler).GetTypeInfo();
|
var type = typeof(PageWithHandler);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type);
|
var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(),
|
||||||
|
new RazorPageAttribute("/Pages/Index", type, typeof(PocoModel), ""));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name));
|
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; }
|
public int IgnoreMe { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateBoundProperties_SupportsGet_OnClass()
|
public void CreateBoundProperties_SupportsGet_OnClass()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue