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