Redesign RazorViewEngine caching

Fixes #3337
This commit is contained in:
Pranav K 2015-10-20 11:49:34 -07:00
parent 845b86963e
commit 380a93d370
34 changed files with 1494 additions and 1805 deletions

View File

@ -10,83 +10,63 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
/// <summary> /// <summary>
/// Represents the result of compilation. /// Represents the result of compilation.
/// </summary> /// </summary>
public class CompilationResult public struct CompilationResult
{ {
/// <summary> /// <summary>
/// Creates a new instance of <see cref="CompilationResult"/>. /// Initializes a new instance of <see cref="CompilationResult"/> for a successful compilation.
/// </summary>
protected CompilationResult()
{
}
/// <summary>
/// Gets (or sets in derived types) the type produced as a result of compilation.
/// </summary>
/// <remarks>This property is <c>null</c> when compilation failed.</remarks>
public Type CompiledType { get; protected set; }
/// <summary>
/// Gets (or sets in derived types) the generated C# content that was compiled.
/// </summary>
public string CompiledContent { get; protected set; }
/// <summary>
/// Gets the <see cref="CompilationFailure"/>s produced from parsing or compiling the Razor file.
/// </summary>
/// <remarks>This property is <c>null</c> when compilation succeeded. An empty sequence
/// indicates a failed compilation.</remarks>
public IEnumerable<CompilationFailure> CompilationFailures { get; private set; }
/// <summary>
/// Gets the <see cref="CompilationResult"/>.
/// </summary>
/// <returns>The current <see cref="CompilationResult"/> instance.</returns>
/// <exception cref="CompilationFailedException">Thrown if compilation failed.</exception>
public CompilationResult EnsureSuccessful()
{
if (CompilationFailures != null)
{
throw new CompilationFailedException(CompilationFailures);
}
return this;
}
/// <summary>
/// Creates a <see cref="CompilationResult"/> for a failed compilation.
/// </summary>
/// <param name="compilationFailures"><see cref="CompilationFailure"/>s produced from parsing or
/// compiling the Razor file.</param>
/// <returns>A <see cref="CompilationResult"/> instance for a failed compilation.</returns>
public static CompilationResult Failed(IEnumerable<CompilationFailure> compilationFailures)
{
if (compilationFailures == null)
{
throw new ArgumentNullException(nameof(compilationFailures));
}
return new CompilationResult
{
CompilationFailures = compilationFailures
};
}
/// <summary>
/// Creates a <see cref="CompilationResult"/> for a successful compilation.
/// </summary> /// </summary>
/// <param name="type">The compiled type.</param> /// <param name="type">The compiled type.</param>
/// <returns>A <see cref="CompilationResult"/> instance for a successful compilation.</returns> public CompilationResult(Type type)
public static CompilationResult Successful(Type type)
{ {
if (type == null) if (type == null)
{ {
throw new ArgumentNullException(nameof(type)); throw new ArgumentNullException(nameof(type));
} }
return new CompilationResult CompiledType = type;
CompilationFailures = null;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilationResult"/> for a failed compilation.
/// </summary>
/// <param name="compilationFailures"><see cref="CompilationFailure"/>s produced from parsing or
/// compiling the Razor file.</param>
public CompilationResult(IEnumerable<CompilationFailure> compilationFailures)
{
if (compilationFailures == null)
{ {
CompiledType = type throw new ArgumentNullException(nameof(compilationFailures));
}; }
CompiledType = null;
CompilationFailures = compilationFailures;
}
/// <summary>
/// Gets the type produced as a result of compilation.
/// </summary>
/// <remarks>This property is <c>null</c> when compilation failed.</remarks>
public Type CompiledType { get; }
/// <summary>
/// Gets the <see cref="CompilationFailure"/>s produced from parsing or compiling the Razor file.
/// </summary>
/// <remarks>This property is <c>null</c> when compilation succeeded. An empty sequence
/// indicates a failed compilation.</remarks>
public IEnumerable<CompilationFailure> CompilationFailures { get; }
/// <summary>
/// Gets the <see cref="CompilationResult"/>.
/// </summary>
/// <returns>The current <see cref="CompilationResult"/> instance.</returns>
/// <exception cref="CompilationFailedException">Thrown if compilation failed.</exception>
public void EnsureSuccessful()
{
if (CompilationFailures != null)
{
throw new CompilationFailedException(CompilationFailures);
}
} }
} }
} }

View File

@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Text; using System.Text;
using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.FileProviders;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor.Compilation namespace Microsoft.AspNet.Mvc.Razor.Compilation
{ {
@ -56,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
foreach (var item in precompiledViews) foreach (var item in precompiledViews)
{ {
var cacheEntry = new CompilerCacheResult(CompilationResult.Successful(item.Value)); var cacheEntry = new CompilerCacheResult(new CompilationResult(item.Value));
_cache.Set(GetNormalizedPath(item.Key), cacheEntry); _cache.Set(GetNormalizedPath(item.Key), cacheEntry);
} }
} }
@ -95,33 +96,29 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
string normalizedPath, string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile) Func<RelativeFileInfo, CompilationResult> compile)
{ {
CompilerCacheResult cacheResult;
var fileInfo = _fileProvider.GetFileInfo(normalizedPath); var fileInfo = _fileProvider.GetFileInfo(normalizedPath);
MemoryCacheEntryOptions cacheEntryOptions; MemoryCacheEntryOptions cacheEntryOptions;
CompilerCacheResult cacheResultToCache; CompilerCacheResult cacheResult;
if (!fileInfo.Exists) if (!fileInfo.Exists)
{ {
cacheResultToCache = CompilerCacheResult.FileNotFound; var expirationToken = _fileProvider.Watch(normalizedPath);
cacheResult = CompilerCacheResult.FileNotFound; cacheResult = new CompilerCacheResult(new[] { expirationToken });
cacheEntryOptions = new MemoryCacheEntryOptions(); cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.AddExpirationToken(_fileProvider.Watch(normalizedPath)); cacheEntryOptions.AddExpirationToken(expirationToken);
} }
else else
{ {
var relativeFileInfo = new RelativeFileInfo(fileInfo, normalizedPath); var relativeFileInfo = new RelativeFileInfo(fileInfo, normalizedPath);
var compilationResult = compile(relativeFileInfo).EnsureSuccessful(); var compilationResult = compile(relativeFileInfo);
compilationResult.EnsureSuccessful();
cacheEntryOptions = GetMemoryCacheEntryOptions(normalizedPath); cacheEntryOptions = GetMemoryCacheEntryOptions(normalizedPath);
cacheResult = new CompilerCacheResult(
// By default the CompilationResult returned by IRoslynCompiler is an instance of compilationResult,
// UncachedCompilationResult. This type has the generated code as a string property and do not want cacheEntryOptions.ExpirationTokens);
// to cache it. We'll instead cache the unwrapped result.
cacheResultToCache = new CompilerCacheResult(
CompilationResult.Successful(compilationResult.CompiledType));
cacheResult = new CompilerCacheResult(compilationResult);
} }
_cache.Set(normalizedPath, cacheResultToCache, cacheEntryOptions); _cache.Set(normalizedPath, cacheResult, cacheEntryOptions);
return cacheResult; return cacheResult;
} }

View File

@ -2,46 +2,77 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor.Compilation namespace Microsoft.AspNet.Mvc.Razor.Compilation
{ {
/// <summary> /// <summary>
/// Result of <see cref="ICompilerCache"/>. /// Result of <see cref="ICompilerCache"/>.
/// </summary> /// </summary>
public class CompilerCacheResult public struct CompilerCacheResult
{ {
/// <summary>
/// Result of <see cref="ICompilerCache"/> when the specified file does not exist in the
/// file system.
/// </summary>
public static CompilerCacheResult FileNotFound { get; } = new CompilerCacheResult();
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified /// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="CompilationResult"/>. /// <see cref="Compilation.CompilationResult"/>.
/// </summary> /// </summary>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/> </param> /// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
public CompilerCacheResult(CompilationResult compilationResult) public CompilerCacheResult(CompilationResult compilationResult)
: this(compilationResult, new IChangeToken[0])
{ {
if (compilationResult == null)
{
throw new ArgumentNullException(nameof(compilationResult));
}
CompilationResult = compilationResult;
} }
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> for a failed file lookup. /// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="Compilation.CompilationResult"/>.
/// </summary> /// </summary>
protected CompilerCacheResult() /// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances that indicate when
/// this result has expired.</param>
public CompilerCacheResult(CompilationResult compilationResult, IList<IChangeToken> expirationTokens)
{ {
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
CompilationResult = compilationResult;
Success = true;
ExpirationTokens = expirationTokens;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> for a file that could not be
/// found in the file system.
/// </summary>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances that indicate when
/// this result has expired.</param>
public CompilerCacheResult(IList<IChangeToken> expirationTokens)
{
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
CompilationResult = default(CompilationResult);
Success = false;
ExpirationTokens = expirationTokens;
} }
/// <summary> /// <summary>
/// The <see cref="Compilation.CompilationResult"/>. /// The <see cref="Compilation.CompilationResult"/>.
/// </summary> /// </summary>
/// <remarks>This property is null when file lookup failed.</remarks> /// <remarks>This property is not available when <see cref="Success"/> is <c>false</c>.</remarks>
public CompilationResult CompilationResult { get; } public CompilationResult CompilationResult { get; }
/// <summary>
/// <see cref="IChangeToken"/> instances that indicate when this result has expired.
/// </summary>
public IList<IChangeToken> ExpirationTokens { get; }
/// <summary>
/// Gets a value that determines if the view was successfully found and compiled.
/// </summary>
public bool Success { get; }
} }
} }

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
failures.Add(compilationFailure); failures.Add(compilationFailure);
} }
return CompilationResult.Failed(failures); return new CompilationResult(failures);
} }
private DiagnosticMessage CreateDiagnosticMessage(RazorError error, string filePath) private DiagnosticMessage CreateDiagnosticMessage(RazorError error, string filePath)

View File

@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var type = assembly.GetExportedTypes() var type = assembly.GetExportedTypes()
.First(t => t.Name.StartsWith(_classPrefix, StringComparison.Ordinal)); .First(t => t.Name.StartsWith(_classPrefix, StringComparison.Ordinal));
return UncachedCompilationResult.Successful(type, compilationContent); return new CompilationResult(type);
} }
} }
} }
@ -214,7 +214,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
failures.Add(compilationFailure); failures.Add(compilationFailure);
} }
return CompilationResult.Failed(failures); return new CompilationResult(failures);
} }
private static string GetFilePath(string relativePath, Diagnostic diagnostic) private static string GetFilePath(string relativePath, Diagnostic diagnostic)

View File

@ -1,47 +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;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
/// <summary>
/// Represents the result of compilation that does not come from the <see cref="ICompilerCache" />.
/// </summary>
public class UncachedCompilationResult : CompilationResult
{
private UncachedCompilationResult()
{
}
public string RazorFileContent { get; private set; }
/// <summary>
/// Creates a <see cref="UncachedCompilationResult"/> that represents a success in compilation.
/// </summary>
/// <param name="type">The compiled type.</param>
/// <param name="compiledContent">The generated C# content that was compiled.</param>
/// <returns>An <see cref="UncachedCompilationResult"/> instance that indicates a successful
/// compilation.</returns>
public static UncachedCompilationResult Successful(
Type type,
string compiledContent)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (compiledContent == null)
{
throw new ArgumentNullException(nameof(compiledContent));
}
return new UncachedCompilationResult
{
CompiledType = type,
CompiledContent = compiledContent,
};
}
}
}

View File

@ -7,10 +7,10 @@ using Microsoft.AspNet.Mvc.Razor.Compilation;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Represents a <see cref="IRazorPageFactory"/> that creates <see cref="RazorPage"/> instances /// Represents a <see cref="IRazorPageFactoryProvider"/> that creates <see cref="RazorPage"/> instances
/// from razor files in the file system. /// from razor files in the file system.
/// </summary> /// </summary>
public class VirtualPathRazorPageFactory : IRazorPageFactory public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider
{ {
/// <remarks> /// <remarks>
/// This delegate holds on to an instance of <see cref="IRazorCompilationService"/>. /// This delegate holds on to an instance of <see cref="IRazorCompilationService"/>.
@ -20,11 +20,11 @@ namespace Microsoft.AspNet.Mvc.Razor
private ICompilerCache _compilerCache; private ICompilerCache _compilerCache;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="VirtualPathRazorPageFactory"/>. /// Initializes a new instance of <see cref="DefaultRazorPageFactoryProvider"/>.
/// </summary> /// </summary>
/// <param name="razorCompilationService">The <see cref="IRazorCompilationService"/>.</param> /// <param name="razorCompilationService">The <see cref="IRazorCompilationService"/>.</param>
/// <param name="compilerCacheProvider">The <see cref="ICompilerCacheProvider"/>.</param> /// <param name="compilerCacheProvider">The <see cref="ICompilerCacheProvider"/>.</param>
public VirtualPathRazorPageFactory( public DefaultRazorPageFactoryProvider(
IRazorCompilationService razorCompilationService, IRazorCompilationService razorCompilationService,
ICompilerCacheProvider compilerCacheProvider) ICompilerCacheProvider compilerCacheProvider)
{ {
@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <inheritdoc /> /// <inheritdoc />
public IRazorPage CreateInstance(string relativePath) public RazorPageFactoryResult CreateFactory(string relativePath)
{ {
if (relativePath == null) if (relativePath == null)
{ {
@ -58,18 +58,33 @@ namespace Microsoft.AspNet.Mvc.Razor
// For tilde slash paths, drop the leading ~ to make it work with the underlying IFileProvider. // For tilde slash paths, drop the leading ~ to make it work with the underlying IFileProvider.
relativePath = relativePath.Substring(1); relativePath = relativePath.Substring(1);
} }
var result = CompilerCache.GetOrAdd(relativePath, _compileDelegate); var result = CompilerCache.GetOrAdd(relativePath, _compileDelegate);
if (result.Success)
if (result == CompilerCacheResult.FileNotFound)
{ {
return null; var pageFactory = GetPageFactory(result.CompilationResult.CompiledType, relativePath);
return new RazorPageFactoryResult(pageFactory, result.ExpirationTokens);
} }
else
{
return new RazorPageFactoryResult(result.ExpirationTokens);
}
}
var page = (IRazorPage)Activator.CreateInstance(result.CompilationResult.CompiledType); /// <summary>
page.Path = relativePath; /// Creates a factory for <see cref="IRazorPage"/>.
/// </summary>
return page; /// <param name="compiledType">The <see cref="Type"/> to produce an instance of <see cref="IRazorPage"/>
/// from.</param>
/// <param name="relativePath">The application relative path of the page.</param>
/// <returns>A factory for <paramref name="compiledType"/>.</returns>
protected virtual Func<IRazorPage> GetPageFactory(Type compiledType, string relativePath)
{
return () =>
{
var page = (IRazorPage)Activator.CreateInstance(compiledType);
page.Path = relativePath;
return page;
};
} }
} }
} }

View File

@ -1,176 +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 Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Default implementation of <see cref="IViewLocationCache"/>.
/// </summary>
public class DefaultViewLocationCache : IViewLocationCache
{
// A mapping of keys generated from ViewLocationExpanderContext to view locations.
private readonly ConcurrentDictionary<ViewLocationCacheKey, ViewLocationCacheResult> _cache;
/// <summary>
/// Initializes a new instance of <see cref="DefaultViewLocationCache"/>.
/// </summary>
public DefaultViewLocationCache()
{
_cache = new ConcurrentDictionary<ViewLocationCacheKey, ViewLocationCacheResult>(
ViewLocationCacheKeyComparer.Instance);
}
/// <inheritdoc />
public ViewLocationCacheResult Get(ViewLocationExpanderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var cacheKey = GenerateKey(context, copyViewExpanderValues: false);
ViewLocationCacheResult result;
if (_cache.TryGetValue(cacheKey, out result))
{
return result;
}
return ViewLocationCacheResult.None;
}
/// <inheritdoc />
public void Set(
ViewLocationExpanderContext context,
ViewLocationCacheResult value)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var cacheKey = GenerateKey(context, copyViewExpanderValues: true);
_cache.TryAdd(cacheKey, value);
}
// Internal for unit testing
internal static ViewLocationCacheKey GenerateKey(
ViewLocationExpanderContext context,
bool copyViewExpanderValues)
{
var controller = RazorViewEngine.GetNormalizedRouteValue(
context.ActionContext,
RazorViewEngine.ControllerKey);
var area = RazorViewEngine.GetNormalizedRouteValue(
context.ActionContext,
RazorViewEngine.AreaKey);
var values = context.Values;
if (values != null && copyViewExpanderValues)
{
// When performing a Get, avoid creating a copy of the values dictionary
values = new Dictionary<string, string>(values, StringComparer.Ordinal);
}
return new ViewLocationCacheKey(
context.ViewName,
controller,
area,
context.IsPartial,
values);
}
// Internal for unit testing
internal class ViewLocationCacheKeyComparer : IEqualityComparer<ViewLocationCacheKey>
{
public static readonly ViewLocationCacheKeyComparer Instance = new ViewLocationCacheKeyComparer();
public bool Equals(ViewLocationCacheKey x, ViewLocationCacheKey y)
{
if (x.IsPartial != y.IsPartial ||
!string.Equals(x.ViewName, y.ViewName, StringComparison.Ordinal) ||
!string.Equals(x.ControllerName, y.ControllerName, StringComparison.Ordinal) ||
!string.Equals(x.AreaName, y.AreaName, StringComparison.Ordinal))
{
return false;
}
if (ReferenceEquals(x.Values, y.Values))
{
return true;
}
if (x.Values == null || y.Values == null || (x.Values.Count != y.Values.Count))
{
return false;
}
foreach (var item in x.Values)
{
string yValue;
if (!y.Values.TryGetValue(item.Key, out yValue) ||
!string.Equals(item.Value, yValue, StringComparison.Ordinal))
{
return false;
}
}
return true;
}
public int GetHashCode(ViewLocationCacheKey key)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(key.IsPartial ? 1 : 0);
hashCodeCombiner.Add(key.ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add(key.ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add(key.AreaName, StringComparer.Ordinal);
if (key.Values != null)
{
foreach (var item in key.Values)
{
hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
}
}
return hashCodeCombiner;
}
}
// Internal for unit testing
internal struct ViewLocationCacheKey
{
public ViewLocationCacheKey(
string viewName,
string controllerName,
string areaName,
bool isPartial,
IDictionary<string, string> values)
{
ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
IsPartial = isPartial;
Values = values;
}
public string ViewName { get; }
public string ControllerName { get; }
public string AreaName { get; }
public bool IsPartial { get; }
public IDictionary<string, string> Values { get; }
}
}
}

View File

@ -131,8 +131,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>(); services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>();
// Caches view locations that are valid for the lifetime of the application.
services.TryAddSingleton<IViewLocationCache, DefaultViewLocationCache>();
services.TryAdd(ServiceDescriptor.Singleton<IChunkTreeCache>(serviceProvider => services.TryAdd(ServiceDescriptor.Singleton<IChunkTreeCache>(serviceProvider =>
{ {
var cachedFileProvider = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(); var cachedFileProvider = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
@ -147,10 +145,8 @@ namespace Microsoft.Extensions.DependencyInjection
// In the default scenario the following services are singleton by virtue of being initialized as part of // In the default scenario the following services are singleton by virtue of being initialized as part of
// creating the singleton RazorViewEngine instance. // creating the singleton RazorViewEngine instance.
services.TryAddTransient<IRazorViewFactory, RazorViewFactory>(); services.TryAddTransient<IRazorPageFactoryProvider, DefaultRazorPageFactoryProvider>();
services.TryAddTransient<IRazorPageFactory, VirtualPathRazorPageFactory>();
services.TryAddTransient<IRazorCompilationService, RazorCompilationService>(); services.TryAddTransient<IRazorCompilationService, RazorCompilationService>();
services.TryAddTransient<IViewStartProvider, ViewStartProvider>();
services.TryAddTransient<IMvcRazorHost, MvcRazorHost>(); services.TryAddTransient<IMvcRazorHost, MvcRazorHost>();
// This caches Razor page activation details that are valid for the lifetime of the application. // This caches Razor page activation details that are valid for the lifetime of the application.

View File

@ -6,13 +6,13 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary> /// <summary>
/// Defines methods that are used for creating <see cref="IRazorPage"/> instances at a given path. /// Defines methods that are used for creating <see cref="IRazorPage"/> instances at a given path.
/// </summary> /// </summary>
public interface IRazorPageFactory public interface IRazorPageFactoryProvider
{ {
/// <summary> /// <summary>
/// Creates a <see cref="IRazorPage"/> for the specified path. /// Creates a <see cref="IRazorPage"/> factory for the specified path.
/// </summary> /// </summary>
/// <param name="relativePath">The path to locate the page.</param> /// <param name="relativePath">The path to locate the page.</param>
/// <returns>The IRazorPage instance if it exists, null otherwise.</returns> /// <returns>The <see cref="RazorPageFactoryResult"/> instance.</returns>
IRazorPage CreateInstance(string relativePath); RazorPageFactoryResult CreateFactory(string relativePath);
} }
} }

View File

@ -1,23 +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.AspNet.Mvc.ViewEngines;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Defines methods to create <see cref="RazorView"/> instances with a given <see cref="IRazorPage"/>.
/// </summary>
public interface IRazorViewFactory
{
/// <summary>
/// Creates a <see cref="RazorView"/> providing it with the <see cref="IRazorPage"/> to execute.
/// </summary>
/// <param name="viewEngine">The <see cref="IRazorViewEngine"/> that was used to locate Layout pages
/// that will be part of <paramref name="page"/>'s execution.</param>
/// <param name="page">The <see cref="IRazorPage"/> instance to execute.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
/// <returns>A <see cref="IView"/> instance that renders the contents of the <paramref name="page"/></returns>
IView GetView(IRazorViewEngine viewEngine, IRazorPage page, bool isPartial);
}
}

View File

@ -1,27 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Specifies the contracts for caching view locations generated by <see cref="IViewLocationExpander"/>.
/// </summary>
public interface IViewLocationCache
{
/// <summary>
/// Gets a cached view location based on the specified <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="ViewLocationExpanderContext"/> for the current view location
/// expansion.</param>
/// <returns>The cached location, if available, <c>null</c> otherwise.</returns>
ViewLocationCacheResult Get(ViewLocationExpanderContext context);
/// <summary>
/// Adds a cache entry for values specified by <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="ViewLocationExpanderContext"/> for the current view location
/// expansion.</param>
/// <param name="value">The view location that is to be cached.</param>
void Set(ViewLocationExpanderContext context, ViewLocationCacheResult value);
}
}

View File

@ -1,21 +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.AspNet.Mvc.Razor
{
/// <summary>
/// Defines methods for locating ViewStart pages that are applicable to a page.
/// </summary>
public interface IViewStartProvider
{
/// <summary>
/// Given a view path, returns a sequence of ViewStart instances
/// that are applicable to the specified view.
/// </summary>
/// <param name="path">The path of the page to locate ViewStart files for.</param>
/// <returns>A sequence of <see cref="IRazorPage"/> that represent ViewStart.</returns>
IEnumerable<IRazorPage> GetViewStartPages(string path);
}
}

View File

@ -146,8 +146,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
return null; return null;
} }
return GeneratePrecompiledAssembly(syntaxTrees.Where(tree => tree != null), return GeneratePrecompiledAssembly(
razorFiles.Where(file => file != null)); syntaxTrees.Where(tree => tree != null),
razorFiles.Where(file => file != null));
} }
protected virtual RazorFileInfoCollection GeneratePrecompiledAssembly( protected virtual RazorFileInfoCollection GeneratePrecompiledAssembly(
@ -174,13 +175,15 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
var references = CompileContext.Compilation.References var references = CompileContext.Compilation.References
.Concat(new[] { applicationReference }); .Concat(new[] { applicationReference });
var preCompilationOptions = CompilationSettings.CompilationOptions var preCompilationOptions = CompilationSettings
.WithOutputKind(OutputKind.DynamicallyLinkedLibrary); .CompilationOptions
.WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
var compilation = CSharpCompilation.Create(assemblyResourceName, var compilation = CSharpCompilation.Create(
options: preCompilationOptions, assemblyResourceName,
syntaxTrees: syntaxTrees, options: preCompilationOptions,
references: references); syntaxTrees: syntaxTrees,
references: references);
var generateSymbols = GenerateSymbols && SymbolsUtility.SupportsSymbolsGeneration(); var generateSymbols = GenerateSymbols && SymbolsUtility.SupportsSymbolsGeneration();
// These streams are returned to the runtime and consequently cannot be disposed. // These streams are returned to the runtime and consequently cannot be disposed.
@ -279,9 +282,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
if (results.Success) if (results.Success)
{ {
var syntaxTree = SyntaxTreeGenerator.Generate(results.GeneratedCode, var syntaxTree = SyntaxTreeGenerator.Generate(
fileInfo.FileInfo.PhysicalPath, results.GeneratedCode,
CompilationSettings); fileInfo.FileInfo.PhysicalPath,
CompilationSettings);
var fullTypeName = results.GetMainClassName(host, syntaxTree); var fullTypeName = results.GetMainClassName(host, syntaxTree);
if (fullTypeName != null) if (fullTypeName != null)
@ -297,9 +301,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
} }
else else
{ {
var diagnostics = results.ParserErrors var diagnostics = results
.Select(error => error.ToDiagnostics(fileInfo.FileInfo.PhysicalPath)) .ParserErrors
.ToList(); .Select(error => error.ToDiagnostics(fileInfo.FileInfo.PhysicalPath))
.ToList();
return new PrecompilationCacheEntry(diagnostics); return new PrecompilationCacheEntry(diagnostics);
} }

View File

@ -0,0 +1,72 @@
// 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.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Result of <see cref="IRazorPageFactoryProvider.CreateFactory(string)"/>.
/// </summary>
public struct RazorPageFactoryResult
{
/// <summary>
/// Initializes a new instance of <see cref="RazorPageFactoryResult"/> with the
/// specified <paramref name="expirationTokens"/>.
/// </summary>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances.</param>
public RazorPageFactoryResult(IList<IChangeToken> expirationTokens)
{
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
ExpirationTokens = expirationTokens;
RazorPageFactory = null;
}
/// <summary>
/// Initializes a new instance of <see cref="RazorPageFactoryResult"/> with the
/// specified <see cref="IRazorPage"/> factory.
/// </summary>
/// <param name="razorPageFactory">The <see cref="IRazorPage"/> factory.</param>
/// <param name="expirationTokens">One or more <see cref="IChangeToken"/> instances.</param>
public RazorPageFactoryResult(
Func<IRazorPage> razorPageFactory,
IList<IChangeToken> expirationTokens)
{
if (razorPageFactory == null)
{
throw new ArgumentNullException(nameof(razorPageFactory));
}
if (expirationTokens == null)
{
throw new ArgumentNullException(nameof(expirationTokens));
}
RazorPageFactory = razorPageFactory;
ExpirationTokens = expirationTokens;
}
/// <summary>
/// The <see cref="IRazorPage"/> factory.
/// </summary>
/// <remarks>This property is <c>null</c> when <see cref="Success"/> is <c>false</c>.</remarks>
public Func<IRazorPage> RazorPageFactory { get; }
/// <summary>
/// One or more <see cref="IChangeToken"/>s associated with this instance of
/// <see cref="RazorPageFactoryResult"/>.
/// </summary>
public IList<IChangeToken> ExpirationTokens { get; }
/// <summary>
/// Gets a value that determines if the page was successfully located.
/// </summary>
public bool Success => RazorPageFactory != null;
}
}

View File

@ -7,9 +7,9 @@ using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Represents the results of locating a <see cref="IRazorPage"/>. /// Result of locating a <see cref="IRazorPage"/>.
/// </summary> /// </summary>
public class RazorPageResult public struct RazorPageResult
{ {
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="RazorPageResult"/> for a successful discovery. /// Initializes a new instance of <see cref="RazorPageResult"/> for a successful discovery.
@ -30,6 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor
Name = name; Name = name;
Page = page; Page = page;
SearchedLocations = null;
} }
/// <summary> /// <summary>
@ -50,6 +51,7 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
Name = name; Name = name;
Page = null;
SearchedLocations = searchedLocations; SearchedLocations = searchedLocations;
} }

View File

@ -22,7 +22,6 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private readonly IRazorViewEngine _viewEngine; private readonly IRazorViewEngine _viewEngine;
private readonly IRazorPageActivator _pageActivator; private readonly IRazorPageActivator _pageActivator;
private readonly IViewStartProvider _viewStartProvider;
private readonly HtmlEncoder _htmlEncoder; private readonly HtmlEncoder _htmlEncoder;
private IPageExecutionListenerFeature _pageExecutionFeature; private IPageExecutionListenerFeature _pageExecutionFeature;
@ -31,22 +30,22 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
/// <param name="viewEngine">The <see cref="IRazorViewEngine"/> used to locate Layout pages.</param> /// <param name="viewEngine">The <see cref="IRazorViewEngine"/> used to locate Layout pages.</param>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param> /// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart /// <param name="viewStartPages">The sequence of <see cref="IRazorPage" /> instances executed as _ViewStarts.
/// </param>
/// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param> /// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param>
/// <param name="htmlEncoder">The HTML encoder.</param> /// <param name="htmlEncoder">The HTML encoder.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param> /// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
/// pages</param>
public RazorView( public RazorView(
IRazorViewEngine viewEngine, IRazorViewEngine viewEngine,
IRazorPageActivator pageActivator, IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider, IReadOnlyList<IRazorPage> viewStartPages,
IRazorPage razorPage, IRazorPage razorPage,
HtmlEncoder htmlEncoder, HtmlEncoder htmlEncoder,
bool isPartial) bool isPartial)
{ {
_viewEngine = viewEngine; _viewEngine = viewEngine;
_pageActivator = pageActivator; _pageActivator = pageActivator;
_viewStartProvider = viewStartProvider; ViewStartPages = viewStartPages;
RazorPage = razorPage; RazorPage = razorPage;
_htmlEncoder = htmlEncoder; _htmlEncoder = htmlEncoder;
IsPartial = isPartial; IsPartial = isPartial;
@ -68,6 +67,12 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public bool IsPartial { get; } public bool IsPartial { get; }
/// <summary>
/// Gets the sequence of _ViewStart <see cref="IRazorPage"/> instances
/// that are executed by this view if <see cref="IsPartial"/> is <c>false</c>.
/// </summary>
public IReadOnlyList<IRazorPage> ViewStartPages { get; }
private bool EnableInstrumentation private bool EnableInstrumentation
{ {
get { return _pageExecutionFeature != null; } get { return _pageExecutionFeature != null; }
@ -153,14 +158,13 @@ namespace Microsoft.AspNet.Mvc.Razor
private async Task RenderViewStartAsync(ViewContext context) private async Task RenderViewStartAsync(ViewContext context)
{ {
var viewStarts = _viewStartProvider.GetViewStartPages(RazorPage.Path);
string layout = null; string layout = null;
var oldFilePath = context.ExecutingFilePath; var oldFilePath = context.ExecutingFilePath;
try try
{ {
foreach (var viewStart in viewStarts) for (var i = 0; i < ViewStartPages.Count; i++)
{ {
var viewStart = ViewStartPages[i];
context.ExecutingFilePath = viewStart.Path; context.ExecutingFilePath = viewStart.Path;
// Copy the layout value from the previous view start (if any) to the current. // Copy the layout value from the previous view start (if any) to the current.
viewStart.Layout = layout; viewStart.Layout = layout;

View File

@ -5,10 +5,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.ViewEngines; using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.OptionsModel; using Microsoft.Extensions.OptionsModel;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
@ -22,41 +24,34 @@ namespace Microsoft.AspNet.Mvc.Razor
public class RazorViewEngine : IRazorViewEngine public class RazorViewEngine : IRazorViewEngine
{ {
private const string ViewExtension = ".cshtml"; private const string ViewExtension = ".cshtml";
internal const string ControllerKey = "controller"; private const string ControllerKey = "controller";
internal const string AreaKey = "area"; private const string AreaKey = "area";
private static readonly ViewLocationCacheItem[] EmptyViewStartLocationCacheItems =
new ViewLocationCacheItem[0];
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
private static readonly IEnumerable<string> _viewLocationFormats = new[] private readonly IRazorPageFactoryProvider _pageFactory;
{
"/Views/{1}/{0}" + ViewExtension,
"/Views/Shared/{0}" + ViewExtension,
};
private static readonly IEnumerable<string> _areaViewLocationFormats = new[]
{
"/Areas/{2}/Views/{1}/{0}" + ViewExtension,
"/Areas/{2}/Views/Shared/{0}" + ViewExtension,
"/Views/Shared/{0}" + ViewExtension,
};
private readonly IRazorPageFactory _pageFactory;
private readonly IRazorViewFactory _viewFactory;
private readonly IList<IViewLocationExpander> _viewLocationExpanders; private readonly IList<IViewLocationExpander> _viewLocationExpanders;
private readonly IViewLocationCache _viewLocationCache; private readonly IRazorPageActivator _pageActivator;
private readonly HtmlEncoder _htmlEncoder;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RazorViewEngine" /> class. /// Initializes a new instance of the <see cref="RazorViewEngine" />.
/// </summary> /// </summary>
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param>
public RazorViewEngine( public RazorViewEngine(
IRazorPageFactory pageFactory, IRazorPageFactoryProvider pageFactory,
IRazorViewFactory viewFactory, IRazorPageActivator pageActivator,
IOptions<RazorViewEngineOptions> optionsAccessor, HtmlEncoder htmlEncoder,
IViewLocationCache viewLocationCache) IOptions<RazorViewEngineOptions> optionsAccessor)
{ {
_pageFactory = pageFactory; _pageFactory = pageFactory;
_viewFactory = viewFactory; _pageActivator = pageActivator;
_viewLocationExpanders = optionsAccessor.Value.ViewLocationExpanders; _viewLocationExpanders = optionsAccessor.Value.ViewLocationExpanders;
_viewLocationCache = viewLocationCache; _htmlEncoder = htmlEncoder;
ViewLookupCache = new MemoryCache(new MemoryCacheOptions
{
CompactOnMemoryPressure = false
});
} }
/// <summary> /// <summary>
@ -72,10 +67,11 @@ namespace Microsoft.AspNet.Mvc.Razor
/// For example, the view for the <c>Test</c> action of <c>HomeController</c> should be located at /// For example, the view for the <c>Test</c> action of <c>HomeController</c> should be located at
/// <c>/Views/Home/Test.cshtml</c>. Locations such as <c>/views/home/test.cshtml</c> would not be discovered /// <c>/Views/Home/Test.cshtml</c>. Locations such as <c>/views/home/test.cshtml</c> would not be discovered
/// </remarks> /// </remarks>
public virtual IEnumerable<string> ViewLocationFormats public virtual IEnumerable<string> ViewLocationFormats { get; } = new[]
{ {
get { return _viewLocationFormats; } "/Views/{1}/{0}" + ViewExtension,
} "/Views/Shared/{0}" + ViewExtension,
};
/// <summary> /// <summary>
/// Gets the locations where this instance of <see cref="RazorViewEngine"/> will search for views within an /// Gets the locations where this instance of <see cref="RazorViewEngine"/> will search for views within an
@ -92,15 +88,20 @@ namespace Microsoft.AspNet.Mvc.Razor
/// For example, the view for the <c>Test</c> action of <c>HomeController</c> should be located at /// For example, the view for the <c>Test</c> action of <c>HomeController</c> should be located at
/// <c>/Views/Home/Test.cshtml</c>. Locations such as <c>/views/home/test.cshtml</c> would not be discovered /// <c>/Views/Home/Test.cshtml</c>. Locations such as <c>/views/home/test.cshtml</c> would not be discovered
/// </remarks> /// </remarks>
public virtual IEnumerable<string> AreaViewLocationFormats public virtual IEnumerable<string> AreaViewLocationFormats { get; } = new[]
{ {
get { return _areaViewLocationFormats; } "/Areas/{2}/Views/{1}/{0}" + ViewExtension,
} "/Areas/{2}/Views/Shared/{0}" + ViewExtension,
"/Views/Shared/{0}" + ViewExtension,
};
/// <summary>
/// A cache for results of view lookups.
/// </summary>
protected IMemoryCache ViewLookupCache { get; }
/// <inheritdoc /> /// <inheritdoc />
public ViewEngineResult FindView( public ViewEngineResult FindView(ActionContext context, string viewName)
ActionContext context,
string viewName)
{ {
if (context == null) if (context == null)
{ {
@ -112,14 +113,12 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
} }
var pageResult = GetRazorPageResult(context, viewName, isPartial: false); var pageResult = GetViewLocationCacheResult(context, viewName, isPartial: false);
return CreateViewEngineResult(pageResult, _viewFactory, isPartial: false); return CreateViewEngineResult(pageResult, viewName, isPartial: false);
} }
/// <inheritdoc /> /// <inheritdoc />
public ViewEngineResult FindPartialView( public ViewEngineResult FindPartialView(ActionContext context, string partialViewName)
ActionContext context,
string partialViewName)
{ {
if (context == null) if (context == null)
{ {
@ -131,14 +130,12 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(partialViewName)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(partialViewName));
} }
var pageResult = GetRazorPageResult(context, partialViewName, isPartial: true); var pageResult = GetViewLocationCacheResult(context, partialViewName, isPartial: true);
return CreateViewEngineResult(pageResult, _viewFactory, isPartial: true); return CreateViewEngineResult(pageResult, partialViewName, isPartial: true);
} }
/// <inheritdoc /> /// <inheritdoc />
public RazorPageResult FindPage( public RazorPageResult FindPage(ActionContext context, string pageName)
ActionContext context,
string pageName)
{ {
if (context == null) if (context == null)
{ {
@ -150,7 +147,16 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName));
} }
return GetRazorPageResult(context, pageName, isPartial: true); var cacheResult = GetViewLocationCacheResult(context, pageName, isPartial: true);
if (cacheResult.Success)
{
var razorPage = cacheResult.ViewEntry.PageFactory();
return new RazorPageResult(pageName, razorPage);
}
else
{
return new RazorPageResult(pageName, cacheResult.SearchedLocations);
}
} }
/// <summary> /// <summary>
@ -166,9 +172,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <see cref="Abstractions.ActionDescriptor.RouteConstraints"/> for traditional routes to get route values /// <see cref="Abstractions.ActionDescriptor.RouteConstraints"/> for traditional routes to get route values
/// produces consistently cased results. /// produces consistently cased results.
/// </remarks> /// </remarks>
public static string GetNormalizedRouteValue( public static string GetNormalizedRouteValue(ActionContext context, string key)
ActionContext context,
string key)
{ {
if (context == null) if (context == null)
{ {
@ -228,26 +232,14 @@ namespace Microsoft.AspNet.Mvc.Razor
return stringRouteValue; return stringRouteValue;
} }
private RazorPageResult GetRazorPageResult( private ViewLocationCacheResult GetViewLocationCacheResult(
ActionContext context, ActionContext context,
string pageName, string pageName,
bool isPartial) bool isPartial)
{ {
if (IsApplicationRelativePath(pageName)) if (IsApplicationRelativePath(pageName))
{ {
var applicationRelativePath = pageName; return LocatePageFromPath(pageName, isPartial);
if (!pageName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase))
{
applicationRelativePath += ViewExtension;
}
var page = _pageFactory.CreateInstance(applicationRelativePath);
if (page != null)
{
return new RazorPageResult(pageName, page);
}
return new RazorPageResult(pageName, new[] { pageName });
} }
else else
{ {
@ -255,105 +247,221 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
} }
private RazorPageResult LocatePageFromViewLocations( private ViewLocationCacheResult LocatePageFromPath(string pageName, bool isPartial)
ActionContext context, {
var applicationRelativePath = pageName;
if (!pageName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase))
{
applicationRelativePath += ViewExtension;
}
var cacheKey = new ViewLocationCacheKey(applicationRelativePath, isPartial);
ViewLocationCacheResult cacheResult;
if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult))
{
var expirationTokens = new HashSet<IChangeToken>();
cacheResult = CreateCacheResult(cacheKey, expirationTokens, applicationRelativePath, isPartial);
var cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
foreach (var expirationToken in expirationTokens)
{
cacheEntryOptions.AddExpirationToken(expirationToken);
}
// No views were found at the specified location. Create a not found result.
if (cacheResult == null)
{
cacheResult = new ViewLocationCacheResult(new[] { pageName });
}
cacheResult = ViewLookupCache.Set<ViewLocationCacheResult>(
cacheKey,
cacheResult,
cacheEntryOptions);
}
return cacheResult;
}
private ViewLocationCacheResult LocatePageFromViewLocations(
ActionContext actionContext,
string pageName, string pageName,
bool isPartial) bool isPartial)
{ {
// Initialize the dictionary for the typical case of having controller and action tokens. var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
var areaName = GetNormalizedRouteValue(context, AreaKey); var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
var expanderContext = new ViewLocationExpanderContext(
actionContext,
pageName,
controllerName,
areaName,
isPartial);
Dictionary<string, string> expanderValues = null;
// Only use the area view location formats if we have an area token.
var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats :
ViewLocationFormats;
var expanderContext = new ViewLocationExpanderContext(context, pageName, isPartial);
if (_viewLocationExpanders.Count > 0) if (_viewLocationExpanders.Count > 0)
{ {
expanderContext.Values = new Dictionary<string, string>(StringComparer.Ordinal); expanderValues = new Dictionary<string, string>(StringComparer.Ordinal);
expanderContext.Values = expanderValues;
// 1. Populate values from viewLocationExpanders.
// Perf: Avoid allocations // Perf: Avoid allocations
for( var i = 0; i < _viewLocationExpanders.Count; i++) for (var i = 0; i < _viewLocationExpanders.Count; i++)
{ {
_viewLocationExpanders[i].PopulateValues(expanderContext); _viewLocationExpanders[i].PopulateValues(expanderContext);
} }
} }
// 2. With the values that we've accumumlated so far, check if we have a cached result. var cacheKey = new ViewLocationCacheKey(
IEnumerable<string> locationsToSearch = null; expanderContext.ViewName,
var cachedResult = _viewLocationCache.Get(expanderContext); expanderContext.ControllerName,
if (!cachedResult.Equals(ViewLocationCacheResult.None)) expanderContext.ViewName,
{ expanderContext.IsPartial,
if (cachedResult.IsFoundResult) expanderValues);
{
var page = _pageFactory.CreateInstance(cachedResult.ViewLocation);
if (page != null) ViewLocationCacheResult cacheResult;
{ if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult))
// 2a We have a cache entry where a view was previously found. {
return new RazorPageResult(pageName, page); cacheResult = OnCacheMiss(expanderContext, cacheKey);
}
}
else
{
locationsToSearch = cachedResult.SearchedLocations;
}
} }
if (locationsToSearch == null) return cacheResult;
}
private ViewLocationCacheResult OnCacheMiss(
ViewLocationExpanderContext expanderContext,
ViewLocationCacheKey cacheKey)
{
// Only use the area view location formats if we have an area token.
var viewLocations = !string.IsNullOrEmpty(expanderContext.AreaName) ?
AreaViewLocationFormats :
ViewLocationFormats;
for (var i = 0; i < _viewLocationExpanders.Count; i++)
{ {
// 2b. We did not find a cached location or did not find a IRazorPage at the cached location. viewLocations = _viewLocationExpanders[i].ExpandViewLocations(expanderContext, viewLocations);
// The cached value has expired and we need to look up the page.
foreach (var expander in _viewLocationExpanders)
{
viewLocations = expander.ExpandViewLocations(expanderContext, viewLocations);
}
var controllerName = GetNormalizedRouteValue(context, ControllerKey);
locationsToSearch = viewLocations.Select(
location => string.Format(
CultureInfo.InvariantCulture,
location,
pageName,
controllerName,
areaName
));
} }
// 3. Use the expanded locations to look up a page. ViewLocationCacheResult cacheResult = null;
var searchedLocations = new List<string>(); var searchedLocations = new List<string>();
foreach (var path in locationsToSearch) var expirationTokens = new HashSet<IChangeToken>();
foreach (var location in viewLocations)
{ {
var page = _pageFactory.CreateInstance(path); var path = string.Format(
if (page != null) CultureInfo.InvariantCulture,
location,
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.AreaName);
cacheResult = CreateCacheResult(cacheKey, expirationTokens, path, expanderContext.IsPartial);
if (cacheResult != null)
{ {
// 3a. We found a page. Cache the set of values that produced it and return a found result. break;
_viewLocationCache.Set(expanderContext, new ViewLocationCacheResult(path, searchedLocations));
return new RazorPageResult(pageName, page);
} }
searchedLocations.Add(path); searchedLocations.Add(path);
} }
// 3b. We did not find a page for any of the paths. // No views were found at the specified location. Create a not found result.
_viewLocationCache.Set(expanderContext, new ViewLocationCacheResult(searchedLocations)); if (cacheResult == null)
return new RazorPageResult(pageName, searchedLocations); {
cacheResult = new ViewLocationCacheResult(searchedLocations);
}
var cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.SetSlidingExpiration(_cacheExpirationDuration);
foreach (var expirationToken in expirationTokens)
{
cacheEntryOptions.AddExpirationToken(expirationToken);
}
return ViewLookupCache.Set<ViewLocationCacheResult>(cacheKey, cacheResult, cacheEntryOptions);
}
private ViewLocationCacheResult CreateCacheResult(
ViewLocationCacheKey cacheKey,
HashSet<IChangeToken> expirationTokens,
string relativePath,
bool isPartial)
{
var factoryResult = _pageFactory.CreateFactory(relativePath);
if (factoryResult.ExpirationTokens != null)
{
for (var i = 0; i < factoryResult.ExpirationTokens.Count; i++)
{
expirationTokens.Add(factoryResult.ExpirationTokens[i]);
}
}
if (factoryResult.Success)
{
// Don't need to lookup _ViewStarts for partials.
var viewStartPages = isPartial ?
EmptyViewStartLocationCacheItems :
GetViewStartPages(relativePath, expirationTokens);
return new ViewLocationCacheResult(
new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),
viewStartPages);
}
return null;
}
private IReadOnlyList<ViewLocationCacheItem> GetViewStartPages(
string path,
HashSet<IChangeToken> expirationTokens)
{
var viewStartPages = new List<ViewLocationCacheItem>();
foreach (var viewStartPath in ViewHierarchyUtility.GetViewStartLocations(path))
{
var result = _pageFactory.CreateFactory(viewStartPath);
if (result.ExpirationTokens != null)
{
for (var i = 0; i < result.ExpirationTokens.Count; i++)
{
expirationTokens.Add(result.ExpirationTokens[i]);
}
}
if (result.Success)
{
// Populate the viewStartPages list so that _ViewStarts appear in the order the need to be
// executed (closest last, furthest first). This is the reverse order in which
// ViewHierarchyUtility.GetViewStartLocations returns _ViewStarts.
viewStartPages.Insert(0, new ViewLocationCacheItem(result.RazorPageFactory, viewStartPath));
}
}
return viewStartPages;
} }
private ViewEngineResult CreateViewEngineResult( private ViewEngineResult CreateViewEngineResult(
RazorPageResult result, ViewLocationCacheResult result,
IRazorViewFactory razorViewFactory, string viewName,
bool isPartial) bool isPartial)
{ {
if (result.SearchedLocations != null) if (!result.Success)
{ {
return ViewEngineResult.NotFound(result.Name, result.SearchedLocations); return ViewEngineResult.NotFound(viewName, result.SearchedLocations);
} }
var view = razorViewFactory.GetView(this, result.Page, isPartial); var page = result.ViewEntry.PageFactory();
return ViewEngineResult.Found(result.Name, view); var viewStarts = new IRazorPage[result.ViewStartEntries.Count];
for (var i = 0; i < viewStarts.Length; i++)
{
var viewStartItem = result.ViewStartEntries[i];
viewStarts[i] = result.ViewStartEntries[i].PageFactory();
}
var view = new RazorView(
this,
_pageActivator,
viewStarts,
page,
_htmlEncoder,
isPartial);
return ViewEngineResult.Found(viewName, view);
} }
private static bool IsApplicationRelativePath(string name) private static bool IsApplicationRelativePath(string name)

View File

@ -1,62 +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.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.ViewEngines;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Represents the default <see cref="IRazorViewFactory"/> implementation that creates
/// <see cref="RazorView"/> instances with a given <see cref="IRazorPage"/>.
/// </summary>
public class RazorViewFactory : IRazorViewFactory
{
private readonly HtmlEncoder _htmlEncoder;
private readonly IRazorPageActivator _pageActivator;
private readonly IViewStartProvider _viewStartProvider;
/// <summary>
/// Initializes a new instance of RazorViewFactory
/// </summary>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// pages</param>
public RazorViewFactory(
IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider,
HtmlEncoder htmlEncoder)
{
_pageActivator = pageActivator;
_viewStartProvider = viewStartProvider;
_htmlEncoder = htmlEncoder;
}
/// <inheritdoc />
public IView GetView(
IRazorViewEngine viewEngine,
IRazorPage page,
bool isPartial)
{
if (viewEngine == null)
{
throw new ArgumentNullException(nameof(viewEngine));
}
if (page == null)
{
throw new ArgumentNullException(nameof(page));
}
var razorView = new RazorView(
viewEngine,
_pageActivator,
_viewStartProvider,
page,
_htmlEncoder,
isPartial);
return razorView;
}
}
}

View File

@ -0,0 +1,34 @@
// 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.AspNet.Mvc.Razor
{
/// <summary>
/// An item in <see cref="ViewLocationCacheResult"/>.
/// </summary>
public struct ViewLocationCacheItem
{
/// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheItem"/>.
/// </summary>
/// <param name="razorPageFactory">The <see cref="IRazorPage"/> factory.</param>
/// <param name="location">The application relative path of the <see cref="IRazorPage"/>.</param>
public ViewLocationCacheItem(Func<IRazorPage> razorPageFactory, string location)
{
PageFactory = razorPageFactory;
Location = location;
}
/// <summary>
/// Gets the application relative path of the <see cref="IRazorPage"/>
/// </summary>
public string Location { get; }
/// <summary>
/// Gets the <see cref="IRazorPage"/> factory.
/// </summary>
public Func<IRazorPage> PageFactory { get; }
}
}

View File

@ -0,0 +1,147 @@
// 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.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Key for entries in <see cref="RazorViewEngine.ViewLookupCache"/>.
/// </summary>
public struct ViewLocationCacheKey : IEquatable<ViewLocationCacheKey>
{
/// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
/// </summary>
/// <param name="viewName">The view name or path.</param>
/// <param name="isPartial">Determines if the view is a partial.</param>
public ViewLocationCacheKey(
string viewName,
bool isPartial)
: this(
viewName,
controllerName: null,
areaName: null,
isPartial: isPartial,
values: null)
{
}
/// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheKey"/>.
/// </summary>
/// <param name="viewName">The view name.</param>
/// <param name="controllerName">The controller name.</param>
/// <param name="areaName">The area name.</param>
/// <param name="isPartial">Determines if the view is a partial.</param>
/// <param name="values">Values from <see cref="IViewLocationExpander"/> instances.</param>
public ViewLocationCacheKey(
string viewName,
string controllerName,
string areaName,
bool isPartial,
IReadOnlyDictionary<string, string> values)
{
ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
IsPartial = isPartial;
ViewLocationExpanderValues = values;
}
/// <summary>
/// Gets the view name.
/// </summary>
public string ViewName { get; }
/// <summary>
/// Gets the controller name.
/// </summary>
public string ControllerName { get; }
/// <summary>
/// Gets the area name.
/// </summary>
public string AreaName { get; }
/// <summary>
/// Determines if the view is a partial.
/// </summary>
public bool IsPartial { get; }
/// <summary>
/// Gets the values populated by <see cref="IViewLocationExpander"/> instances.
/// </summary>
public IReadOnlyDictionary<string, string> ViewLocationExpanderValues { get; }
/// <inheritdoc />
public bool Equals(ViewLocationCacheKey y)
{
if (IsPartial != y.IsPartial ||
!string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
!string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
!string.Equals(AreaName, y.AreaName, StringComparison.Ordinal))
{
return false;
}
if (ReferenceEquals(ViewLocationExpanderValues, y.ViewLocationExpanderValues))
{
return true;
}
if (ViewLocationExpanderValues == null ||
y.ViewLocationExpanderValues == null ||
(ViewLocationExpanderValues.Count != y.ViewLocationExpanderValues.Count))
{
return false;
}
foreach (var item in ViewLocationExpanderValues)
{
string yValue;
if (!y.ViewLocationExpanderValues.TryGetValue(item.Key, out yValue) ||
!string.Equals(item.Value, yValue, StringComparison.Ordinal))
{
return false;
}
}
return true;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is ViewLocationCacheKey)
{
return Equals((ViewLocationCacheKey)obj);
}
return false;
}
/// <inheritdoc />
public override int GetHashCode()
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(IsPartial ? 1 : 0);
hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);
if (ViewLocationExpanderValues != null)
{
foreach (var item in ViewLocationExpanderValues)
{
hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
}
}
return hashCodeCombiner;
}
}
}

View File

@ -3,36 +3,32 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Result of <see cref="IViewLocationCache"/> lookups. /// Result of view location cache lookup.
/// </summary> /// </summary>
public struct ViewLocationCacheResult : IEquatable<ViewLocationCacheResult> public class ViewLocationCacheResult
{ {
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheResult"/> /// Initializes a new instance of <see cref="ViewLocationCacheResult"/>
/// for a view that was successfully found at the specified location. /// for a view that was successfully found at the specified location.
/// </summary> /// </summary>
/// <param name="foundLocation">The view location.</param> /// <param name="view">The <see cref="ViewLocationCacheItem"/> for the found view.</param>
/// <param name="searchedLocations">Locations that were searched /// <param name="viewStarts"><see cref="ViewLocationCacheItem"/>s for applicable _ViewStarts.</param>
/// in addition to <paramref name="foundLocation"/>.</param>
public ViewLocationCacheResult( public ViewLocationCacheResult(
string foundLocation, ViewLocationCacheItem view,
IEnumerable<string> searchedLocations) IReadOnlyList<ViewLocationCacheItem> viewStarts)
: this(searchedLocations)
{ {
if (foundLocation == null) if (viewStarts == null)
{ {
throw new ArgumentNullException(nameof(foundLocation)); throw new ArgumentNullException(nameof(viewStarts));
} }
ViewLocation = foundLocation; ViewEntry = view;
SearchedLocations = searchedLocations; ViewStartEntries = viewStarts;
IsFoundResult = true; Success = true;
} }
/// <summary> /// <summary>
@ -48,27 +44,26 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
SearchedLocations = searchedLocations; SearchedLocations = searchedLocations;
ViewLocation = null;
IsFoundResult = false;
} }
/// <summary> /// <summary>
/// A <see cref="ViewLocationCacheResult"/> that represents a cache miss. /// <see cref="ViewLocationCacheItem"/> for the located view.
/// </summary> /// </summary>
public static readonly ViewLocationCacheResult None = new ViewLocationCacheResult(Enumerable.Empty<string>()); /// <remarks><c>null</c> if <see cref="Success"/> is <c>false</c>.</remarks>
public ViewLocationCacheItem ViewEntry { get; }
/// <summary> /// <summary>
/// The location the view was found. /// <see cref="ViewLocationCacheItem"/>s for applicable _ViewStarts.
/// </summary> /// </summary>
/// <remarks>This is available if <see cref="IsFoundResult"/> is <c>true</c>.</remarks> /// <remarks><c>null</c> if <see cref="Success"/> is <c>false</c>.</remarks>
public string ViewLocation { get; } public IReadOnlyList<ViewLocationCacheItem> ViewStartEntries { get; }
/// <summary> /// <summary>
/// The sequence of locations that were searched. /// The sequence of locations that were searched.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When <see cref="IsFoundResult"/> is <c>true</c> this includes all paths that were search prior to finding /// When <see cref="Success"/> is <c>true</c> this includes all paths that were search prior to finding
/// a view at <see cref="ViewLocation"/>. When <see cref="IsFoundResult"/> is <c>false</c>, this includes /// a view at <see cref="ViewEntry"/>. When <see cref="Success"/> is <c>false</c>, this includes
/// all search paths. /// all search paths.
/// </remarks> /// </remarks>
public IEnumerable<string> SearchedLocations { get; } public IEnumerable<string> SearchedLocations { get; }
@ -76,55 +71,6 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary> /// <summary>
/// Gets a value that indicates whether the view was successfully found. /// Gets a value that indicates whether the view was successfully found.
/// </summary> /// </summary>
public bool IsFoundResult { get; } public bool Success { get; }
/// <inheritdoc />
public bool Equals(ViewLocationCacheResult other)
{
if (IsFoundResult != other.IsFoundResult)
{
return false;
}
if (IsFoundResult)
{
return string.Equals(ViewLocation, other.ViewLocation, StringComparison.Ordinal);
}
else
{
if (SearchedLocations == other.SearchedLocations)
{
return true;
}
if (SearchedLocations == null || other.SearchedLocations == null)
{
return false;
}
return Enumerable.SequenceEqual(SearchedLocations, other.SearchedLocations, StringComparer.Ordinal);
}
}
/// <inheritdoc />
public override int GetHashCode()
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(IsFoundResult);
if (IsFoundResult)
{
hashCodeCombiner.Add(ViewLocation, StringComparer.Ordinal);
}
else if (SearchedLocations != null)
{
foreach (var location in SearchedLocations)
{
hashCodeCombiner.Add(location, StringComparer.Ordinal);
}
}
return hashCodeCombiner;
}
} }
} }

View File

@ -16,10 +16,14 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current executing action.</param> /// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current executing action.</param>
/// <param name="viewName">The view name.</param> /// <param name="viewName">The view name.</param>
/// <param name="controllerName">The controller name.</param>
/// <param name="areaName">The area name.</param>
/// <param name="isPartial">Determines if the view being discovered is a partial.</param> /// <param name="isPartial">Determines if the view being discovered is a partial.</param>
public ViewLocationExpanderContext( public ViewLocationExpanderContext(
ActionContext actionContext, ActionContext actionContext,
string viewName, string viewName,
string controllerName,
string areaName,
bool isPartial) bool isPartial)
{ {
if (actionContext == null) if (actionContext == null)
@ -34,6 +38,8 @@ namespace Microsoft.AspNet.Mvc.Razor
ActionContext = actionContext; ActionContext = actionContext;
ViewName = viewName; ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
IsPartial = isPartial; IsPartial = isPartial;
} }
@ -47,6 +53,16 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public string ViewName { get; } public string ViewName { get; }
/// <summary>
/// Gets the controller name.
/// </summary>
public string ControllerName { get; }
/// <summary>
/// Gets the area name.
/// </summary>
public string AreaName { get; }
/// <summary> /// <summary>
/// Gets a value that determines if a partial view is being discovered. /// Gets a value that determines if a partial view is being discovered.
/// </summary> /// </summary>

View File

@ -1,41 +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;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <inheritdoc />
public class ViewStartProvider : IViewStartProvider
{
private readonly IRazorPageFactory _pageFactory;
public ViewStartProvider(IRazorPageFactory pageFactory)
{
_pageFactory = pageFactory;
}
/// <inheritdoc />
public IEnumerable<IRazorPage> GetViewStartPages(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
var viewStartLocations = ViewHierarchyUtility.GetViewStartLocations(path);
var viewStarts = viewStartLocations.Select(_pageFactory.CreateInstance)
.Where(p => p != null)
.ToArray();
// GetViewStartLocations return ViewStarts inside-out that is the _ViewStart closest to the page
// is the first: e.g. [ /Views/Home/_ViewStart, /Views/_ViewStart, /_ViewStart ]
// However they need to be executed outside in, so we'll reverse the sequence.
Array.Reverse(viewStarts);
return viewStarts;
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
// Arrange // Arrange
var compilationFailure = new CompilationFailure("test", Enumerable.Empty<Microsoft.Extensions.PlatformAbstractions.DiagnosticMessage>()); var compilationFailure = new CompilationFailure("test", Enumerable.Empty<Microsoft.Extensions.PlatformAbstractions.DiagnosticMessage>());
var failures = new[] { compilationFailure }; var failures = new[] { compilationFailure };
var result = CompilationResult.Failed(failures); var result = new CompilationResult(failures);
// Act and Assert // Act and Assert
Assert.Null(result.CompiledType); Assert.Null(result.CompiledType);

View File

@ -36,8 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result = cache.GetOrAdd("/some/path", ThrowsIfCalled); var result = cache.GetOrAdd("/some/path", ThrowsIfCalled);
// Assert // Assert
Assert.Same(CompilerCacheResult.FileNotFound, result); Assert.False(result.Success);
Assert.Null(result.CompilationResult);
} }
[Fact] [Fact]
@ -48,18 +47,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider); var cache = new CompilerCache(fileProvider);
var type = typeof(TestView); var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = new CompilationResult(type);
// Act // Act
var result = cache.GetOrAdd(ViewPath, _ => expected); var result = cache.GetOrAdd(ViewPath, _ => expected);
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result); Assert.True(result.Success);
var actual = Assert.IsType<UncachedCompilationResult>(result.CompilationResult); Assert.Same(type, result.CompilationResult.CompiledType);
Assert.NotNull(actual);
Assert.Same(expected, actual);
Assert.Equal("hello world", actual.CompiledContent);
Assert.Same(type, actual.CompiledType);
} }
[Theory] [Theory]
@ -75,15 +70,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
fileProvider.AddFile(viewPath, "some content"); fileProvider.AddFile(viewPath, "some content");
var cache = new CompilerCache(fileProvider); var cache = new CompilerCache(fileProvider);
var type = typeof(TestView); var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = new CompilationResult(type);
// Act - 1 // Act - 1
var result1 = cache.GetOrAdd(@"Areas\Finances\Views\Home\Index.cshtml", _ => expected); var result1 = cache.GetOrAdd(@"Areas\Finances\Views\Home\Index.cshtml", _ => expected);
// Assert - 1 // Assert - 1
var compilationResult = Assert.IsType<UncachedCompilationResult>(result1.CompilationResult); Assert.Same(type, result1.CompilationResult.CompiledType);
Assert.Same(expected, compilationResult);
Assert.Same(type, compilationResult.CompiledType);
// Act - 2 // Act - 2
var result2 = cache.GetOrAdd(relativePath, ThrowsIfCalled); var result2 = cache.GetOrAdd(relativePath, ThrowsIfCalled);
@ -93,21 +86,21 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
} }
[Fact] [Fact]
public void GetOrAdd_ReturnsFileNotFoundIfFileWasDeleted() public void GetOrAdd_ReturnsFailedCompilationResult_IfFileWasRemovedFromFileSystem()
{ {
// Arrange // Arrange
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider); var cache = new CompilerCache(fileProvider);
var type = typeof(TestView); var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = new CompilationResult(type);
// Act 1 // Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected); var result1 = cache.GetOrAdd(ViewPath, _ => expected);
// Assert 1 // Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.True(result1.Success);
Assert.Same(expected, result1.CompilationResult); Assert.Same(expected.CompiledType, result1.CompilationResult.CompiledType);
// Act 2 // Act 2
// Delete the file from the file system and set it's expiration token. // Delete the file from the file system and set it's expiration token.
@ -116,8 +109,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2 // Assert 2
Assert.Same(CompilerCacheResult.FileNotFound, result2); Assert.False(result2.Success);
Assert.Null(result2.CompilationResult);
} }
[Fact] [Fact]
@ -127,22 +119,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider); var cache = new CompilerCache(fileProvider);
var expected1 = UncachedCompilationResult.Successful(typeof(TestView), "hello world"); var expected1 = new CompilationResult(typeof(TestView));
var expected2 = UncachedCompilationResult.Successful(typeof(DifferentView), "different content"); var expected2 = new CompilationResult(typeof(DifferentView));
// Act 1 // Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1); var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
// Assert 1 // Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.True(result1.Success);
Assert.Same(expected1, result1.CompilationResult); Assert.Same(typeof(TestView), result1.CompilationResult.CompiledType);
// Act 2 // Act 2
// Verify we're getting cached results. // Verify we're getting cached results.
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.True(result2.Success);
Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType); Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType);
// Act 3 // Act 3
@ -150,8 +142,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result3 = cache.GetOrAdd(ViewPath, _ => expected2); var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
// Assert 3 // Assert 3
Assert.NotSame(CompilerCacheResult.FileNotFound, result3); Assert.True(result3.Success);
Assert.Same(expected2, result3.CompilationResult); Assert.Same(expected2.CompiledType, result3.CompilationResult.CompiledType);
} }
[Theory] [Theory]
@ -162,22 +154,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider); var cache = new CompilerCache(fileProvider);
var expected1 = UncachedCompilationResult.Successful(typeof(TestView), "hello world"); var expected1 = new CompilationResult(typeof(TestView));
var expected2 = UncachedCompilationResult.Successful(typeof(DifferentView), "different content"); var expected2 = new CompilationResult(typeof(DifferentView));
// Act 1 // Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1); var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
// Assert 1 // Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.True(result1.Success);
Assert.Same(expected1, result1.CompilationResult); Assert.Same(expected1.CompiledType, result1.CompilationResult.CompiledType);
// Act 2 // Act 2
// Verify we're getting cached results. // Verify we're getting cached results.
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.True(result2.Success);
Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType); Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType);
// Act 3 // Act 3
@ -185,8 +177,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result3 = cache.GetOrAdd(ViewPath, _ => expected2); var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result3); Assert.True(result3.Success);
Assert.Same(expected2, result3.CompilationResult); Assert.Same(expected2.CompiledType, result3.CompilationResult.CompiledType);
} }
[Fact] [Fact]
@ -198,21 +190,20 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider); var cache = new CompilerCache(fileProvider);
var type = typeof(TestView); var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = new CompilationResult(type);
// Act 1 // Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected); var result1 = cache.GetOrAdd(ViewPath, _ => expected);
// Assert 1 // Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1); Assert.True(result1.Success);
Assert.Same(expected, result1.CompilationResult); Assert.Same(type, result1.CompilationResult.CompiledType);
// Act 2 // Act 2
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.True(result2.Success);
Assert.IsType<CompilationResult>(result2.CompilationResult);
Assert.Same(type, result2.CompilationResult.CompiledType); Assert.Same(type, result2.CompilationResult.CompiledType);
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once()); mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
} }
@ -228,7 +219,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result); Assert.True(result.Success);
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType); Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
} }
@ -245,7 +236,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result); Assert.True(result.Success);
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType); Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
} }
@ -263,7 +254,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result); Assert.True(result.Success);
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType); Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
} }
@ -274,26 +265,26 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(fileProvider, _precompiledViews); var cache = new CompilerCache(fileProvider, _precompiledViews);
var expected = CompilationResult.Successful(typeof(TestView)); var expected = new CompilationResult(typeof(TestView));
// Act 1 // Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected); var result1 = cache.GetOrAdd(ViewPath, _ => expected);
// Assert 1 // Assert 1
Assert.Same(expected, result1.CompilationResult); Assert.Same(typeof(TestView), result1.CompilationResult.CompiledType);
// Act 2 // Act 2
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled); var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.True(result2.Success);
Assert.Same(typeof(TestView), result2.CompilationResult.CompiledType); Assert.Same(typeof(TestView), result2.CompilationResult.CompiledType);
// Act 3 // Act 3
var result3 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled); var result3 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert 3 // Assert 3
Assert.NotSame(CompilerCacheResult.FileNotFound, result2); Assert.True(result2.Success);
Assert.Same(typeof(PreCompile), result3.CompilationResult.CompiledType); Assert.Same(typeof(PreCompile), result3.CompilationResult.CompiledType);
} }

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var compiler = new Mock<ICompilationService>(); var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(relativeFileInfo, It.IsAny<string>())) compiler.Setup(c => c.Compile(relativeFileInfo, It.IsAny<string>()))
.Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest))); .Returns(new CompilationResult(typeof(RazorCompilationServiceTest)));
var razorService = new RazorCompilationService(compiler.Object, host.Object, GetOptions()); var razorService = new RazorCompilationService(compiler.Object, host.Object, GetOptions());
@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
.Returns(Stream.Null); .Returns(Stream.Null);
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml"); var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml");
var compilationResult = CompilationResult.Successful(typeof(object)); var compilationResult = new CompilationResult(typeof(object));
var compiler = new Mock<ICompilationService>(); var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(relativeFileInfo, code)) compiler.Setup(c => c.Compile(relativeFileInfo, code))
.Returns(compilationResult) .Returns(compilationResult)
@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result = razorService.Compile(relativeFileInfo); var result = razorService.Compile(relativeFileInfo);
// Assert // Assert
Assert.Same(compilationResult, result); Assert.Same(compilationResult.CompiledType, result.CompiledType);
compiler.Verify(); compiler.Verify();
} }

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
public class RoslynCompilationServiceTest public class RoslynCompilationServiceTest
{ {
[Fact] [Fact]
public void Compile_ReturnsUncachedCompilationResultWithCompiledContent() public void Compile_ReturnsCompilationResult()
{ {
// Arrange // Arrange
var content = @" var content = @"
@ -29,29 +29,31 @@ public class MyTestType {}";
var libraryExporter = GetLibraryExporter(); var libraryExporter = GetLibraryExporter();
var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>(); var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>();
compilerOptionsProvider.Setup(p => p.GetCompilerOptions(applicationEnvironment.ApplicationName, compilerOptionsProvider
applicationEnvironment.RuntimeFramework, .Setup(p => p.GetCompilerOptions(
applicationEnvironment.Configuration)) applicationEnvironment.ApplicationName,
.Returns(new CompilerOptions()); applicationEnvironment.RuntimeFramework,
applicationEnvironment.Configuration))
.Returns(new CompilerOptions());
var mvcRazorHost = new Mock<IMvcRazorHost>(); var mvcRazorHost = new Mock<IMvcRazorHost>();
mvcRazorHost.SetupGet(m => m.MainClassNamePrefix) mvcRazorHost.SetupGet(m => m.MainClassNamePrefix)
.Returns(string.Empty); .Returns(string.Empty);
var compilationService = new RoslynCompilationService(applicationEnvironment, var compilationService = new RoslynCompilationService(
libraryExporter, applicationEnvironment,
compilerOptionsProvider.Object, libraryExporter,
mvcRazorHost.Object, compilerOptionsProvider.Object,
GetOptions()); mvcRazorHost.Object,
var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { PhysicalPath = "SomePath" }, GetOptions());
var relativeFileInfo = new RelativeFileInfo(
new TestFileInfo { PhysicalPath = "SomePath" },
"some-relative-path"); "some-relative-path");
// Act // Act
var result = compilationService.Compile(relativeFileInfo, content); var result = compilationService.Compile(relativeFileInfo, content);
// Assert // Assert
var uncachedResult = Assert.IsType<UncachedCompilationResult>(result);
Assert.Equal("MyTestType", result.CompiledType.Name); Assert.Equal("MyTestType", result.CompiledType.Name);
Assert.Equal(content, uncachedResult.CompiledContent);
} }
[Fact] [Fact]
@ -67,19 +69,22 @@ this should fail";
var libraryExporter = GetLibraryExporter(); var libraryExporter = GetLibraryExporter();
var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>(); var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>();
compilerOptionsProvider.Setup(p => p.GetCompilerOptions(applicationEnvironment.ApplicationName, compilerOptionsProvider
applicationEnvironment.RuntimeFramework, .Setup(p => p.GetCompilerOptions(
applicationEnvironment.Configuration)) applicationEnvironment.ApplicationName,
.Returns(new CompilerOptions()); applicationEnvironment.RuntimeFramework,
applicationEnvironment.Configuration))
.Returns(new CompilerOptions());
var mvcRazorHost = Mock.Of<IMvcRazorHost>(); var mvcRazorHost = Mock.Of<IMvcRazorHost>();
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(viewPath, fileContent); var fileInfo = fileProvider.AddFile(viewPath, fileContent);
var compilationService = new RoslynCompilationService(applicationEnvironment, var compilationService = new RoslynCompilationService(
libraryExporter, applicationEnvironment,
compilerOptionsProvider.Object, libraryExporter,
mvcRazorHost, compilerOptionsProvider.Object,
GetOptions(fileProvider)); mvcRazorHost,
GetOptions(fileProvider));
var relativeFileInfo = new RelativeFileInfo(fileInfo, "some-relative-path"); var relativeFileInfo = new RelativeFileInfo(fileInfo, "some-relative-path");
// Act // Act
@ -103,18 +108,22 @@ this should fail";
var libraryExporter = GetLibraryExporter(); var libraryExporter = GetLibraryExporter();
var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>(); var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>();
compilerOptionsProvider.Setup(p => p.GetCompilerOptions(applicationEnvironment.ApplicationName, compilerOptionsProvider
applicationEnvironment.RuntimeFramework, .Setup(p => p.GetCompilerOptions(
applicationEnvironment.Configuration)) applicationEnvironment.ApplicationName,
.Returns(new CompilerOptions()); applicationEnvironment.RuntimeFramework,
applicationEnvironment.Configuration))
.Returns(new CompilerOptions());
var mvcRazorHost = Mock.Of<IMvcRazorHost>(); var mvcRazorHost = Mock.Of<IMvcRazorHost>();
var compilationService = new RoslynCompilationService(applicationEnvironment, var compilationService = new RoslynCompilationService(
libraryExporter, applicationEnvironment,
compilerOptionsProvider.Object, libraryExporter,
mvcRazorHost, compilerOptionsProvider.Object,
GetOptions()); mvcRazorHost,
var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { Content = fileContent }, GetOptions());
var relativeFileInfo = new RelativeFileInfo(
new TestFileInfo { Content = fileContent },
"some-relative-path"); "some-relative-path");
// Act // Act
@ -141,10 +150,12 @@ this should fail";
var libraryExporter = GetLibraryExporter(); var libraryExporter = GetLibraryExporter();
var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>(); var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>();
compilerOptionsProvider.Setup(p => p.GetCompilerOptions(applicationEnvironment.ApplicationName, compilerOptionsProvider
applicationEnvironment.RuntimeFramework, .Setup(p => p.GetCompilerOptions(
applicationEnvironment.Configuration)) applicationEnvironment.ApplicationName,
.Returns(new CompilerOptions()); applicationEnvironment.RuntimeFramework,
applicationEnvironment.Configuration))
.Returns(new CompilerOptions());
var mvcRazorHost = Mock.Of<IMvcRazorHost>(); var mvcRazorHost = Mock.Of<IMvcRazorHost>();
var mockFileInfo = new Mock<IFileInfo>(); var mockFileInfo = new Mock<IFileInfo>();
@ -153,11 +164,12 @@ this should fail";
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(path, mockFileInfo.Object); fileProvider.AddFile(path, mockFileInfo.Object);
var compilationService = new RoslynCompilationService(applicationEnvironment, var compilationService = new RoslynCompilationService(
libraryExporter, applicationEnvironment,
compilerOptionsProvider.Object, libraryExporter,
mvcRazorHost, compilerOptionsProvider.Object,
GetOptions(fileProvider)); mvcRazorHost,
GetOptions(fileProvider));
var relativeFileInfo = new RelativeFileInfo(mockFileInfo.Object, path); var relativeFileInfo = new RelativeFileInfo(mockFileInfo.Object, path);
@ -187,20 +199,24 @@ public class MyNonCustomDefinedClass {}
var libraryExporter = GetLibraryExporter(); var libraryExporter = GetLibraryExporter();
var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>(); var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>();
compilerOptionsProvider.Setup(p => p.GetCompilerOptions(applicationEnvironment.ApplicationName, compilerOptionsProvider
applicationEnvironment.RuntimeFramework, .Setup(p => p.GetCompilerOptions(
applicationEnvironment.Configuration)) applicationEnvironment.ApplicationName,
.Returns(new CompilerOptions { Defines = new[] { "MY_CUSTOM_DEFINE" } }); applicationEnvironment.RuntimeFramework,
applicationEnvironment.Configuration))
.Returns(new CompilerOptions { Defines = new[] { "MY_CUSTOM_DEFINE" } });
var mvcRazorHost = new Mock<IMvcRazorHost>(); var mvcRazorHost = new Mock<IMvcRazorHost>();
mvcRazorHost.SetupGet(m => m.MainClassNamePrefix) mvcRazorHost.SetupGet(m => m.MainClassNamePrefix)
.Returns("My"); .Returns("My");
var compilationService = new RoslynCompilationService(applicationEnvironment, var compilationService = new RoslynCompilationService(
libraryExporter, applicationEnvironment,
compilerOptionsProvider.Object, libraryExporter,
mvcRazorHost.Object, compilerOptionsProvider.Object,
GetOptions()); mvcRazorHost.Object,
var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { PhysicalPath = "SomePath" }, GetOptions());
var relativeFileInfo = new RelativeFileInfo(
new TestFileInfo { PhysicalPath = "SomePath" },
"some-relative-path"); "some-relative-path");
// Act // Act
@ -222,21 +238,25 @@ public class NotRazorPrefixType {}";
var libraryExporter = GetLibraryExporter(); var libraryExporter = GetLibraryExporter();
var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>(); var compilerOptionsProvider = new Mock<ICompilerOptionsProvider>();
compilerOptionsProvider.Setup(p => p.GetCompilerOptions(applicationEnvironment.ApplicationName, compilerOptionsProvider
applicationEnvironment.RuntimeFramework, .Setup(p => p.GetCompilerOptions(
applicationEnvironment.Configuration)) applicationEnvironment.ApplicationName,
.Returns(new CompilerOptions()); applicationEnvironment.RuntimeFramework,
applicationEnvironment.Configuration))
.Returns(new CompilerOptions());
var mvcRazorHost = new Mock<IMvcRazorHost>(); var mvcRazorHost = new Mock<IMvcRazorHost>();
mvcRazorHost.SetupGet(m => m.MainClassNamePrefix) mvcRazorHost.SetupGet(m => m.MainClassNamePrefix)
.Returns("RazorPrefix"); .Returns("RazorPrefix");
var compilationService = new RoslynCompilationService(applicationEnvironment, var compilationService = new RoslynCompilationService(
libraryExporter, applicationEnvironment,
compilerOptionsProvider.Object, libraryExporter,
mvcRazorHost.Object, compilerOptionsProvider.Object,
GetOptions()); mvcRazorHost.Object,
GetOptions());
var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { PhysicalPath = "SomePath" }, var relativeFileInfo = new RelativeFileInfo(
new TestFileInfo { PhysicalPath = "SomePath" },
"some-relative-path"); "some-relative-path");
// Act // Act
@ -356,27 +376,33 @@ public class NotRazorPrefixType {}";
private static ILibraryExporter GetLibraryExporter() private static ILibraryExporter GetLibraryExporter()
{ {
var fileReference = new Mock<IMetadataFileReference>(); var fileReference = new Mock<IMetadataFileReference>();
fileReference.SetupGet(f => f.Path) fileReference
.Returns(typeof(string).Assembly.Location); .SetupGet(f => f.Path)
.Returns(typeof(string).Assembly.Location);
var libraryExport = new LibraryExport(fileReference.Object); var libraryExport = new LibraryExport(fileReference.Object);
var libraryExporter = new Mock<ILibraryExporter>(); var libraryExporter = new Mock<ILibraryExporter>();
libraryExporter.Setup(l => l.GetAllExports(It.IsAny<string>())) libraryExporter
.Returns(libraryExport); .Setup(l => l.GetAllExports(It.IsAny<string>()))
.Returns(libraryExport);
return libraryExporter.Object; return libraryExporter.Object;
} }
private IApplicationEnvironment GetApplicationEnvironment() private IApplicationEnvironment GetApplicationEnvironment()
{ {
var applicationEnvironment = new Mock<IApplicationEnvironment>(); var applicationEnvironment = new Mock<IApplicationEnvironment>();
applicationEnvironment.SetupGet(a => a.ApplicationName) applicationEnvironment
.Returns("MyApp"); .SetupGet(a => a.ApplicationName)
applicationEnvironment.SetupGet(a => a.RuntimeFramework) .Returns("MyApp");
.Returns(new FrameworkName("ASPNET", new Version(5, 0))); applicationEnvironment
applicationEnvironment.SetupGet(a => a.Configuration) .SetupGet(a => a.RuntimeFramework)
.Returns("Debug"); .Returns(new FrameworkName("ASPNET", new Version(5, 0)));
applicationEnvironment.SetupGet(a => a.ApplicationBasePath) applicationEnvironment
.Returns("MyBasePath"); .SetupGet(a => a.Configuration)
.Returns("Debug");
applicationEnvironment
.SetupGet(a => a.ApplicationBasePath)
.Returns("MyBasePath");
return applicationEnvironment.Object; return applicationEnvironment.Object;
} }
@ -388,7 +414,8 @@ public class NotRazorPrefixType {}";
FileProvider = fileProvider ?? new TestFileProvider() FileProvider = fileProvider ?? new TestFileProvider()
}; };
var options = new Mock<IOptions<RazorViewEngineOptions>>(); var options = new Mock<IOptions<RazorViewEngineOptions>>();
options.SetupGet(o => o.Value) options
.SetupGet(o => o.Value)
.Returns(razorViewEngineOptions); .Returns(razorViewEngineOptions);
return options.Object; return options.Object;

View File

@ -0,0 +1,106 @@
// 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.Tasks;
using Microsoft.AspNet.Mvc.Razor.Compilation;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Test
{
public class DefaultRazorPageFactoryProviderTest
{
[Fact]
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForUnsuccessfulResults()
{
// Arrange
var expirationTokens = new[]
{
Mock.Of<IChangeToken>(),
Mock.Of<IChangeToken>(),
};
var compilerCache = new Mock<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<RelativeFileInfo, CompilationResult>>()))
.Returns(new CompilerCacheResult(expirationTokens));
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Returns(compilerCache.Object);
var factoryProvider = new DefaultRazorPageFactoryProvider(
Mock.Of<IRazorCompilationService>(),
compilerCacheProvider.Object);
// Act
var result = factoryProvider.CreateFactory("/file-does-not-exist");
// Assert
Assert.False(result.Success);
Assert.Equal(expirationTokens, result.ExpirationTokens);
}
[Fact]
public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForSuccessfulResults()
{
// Arrange
var expirationTokens = new[]
{
Mock.Of<IChangeToken>(),
Mock.Of<IChangeToken>(),
};
var compilerCache = new Mock<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<RelativeFileInfo, CompilationResult>>()))
.Returns(new CompilerCacheResult(new CompilationResult(typeof(object)), expirationTokens));
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Returns(compilerCache.Object);
var factoryProvider = new DefaultRazorPageFactoryProvider(
Mock.Of<IRazorCompilationService>(),
compilerCacheProvider.Object);
// Act
var result = factoryProvider.CreateFactory("/file-exists");
// Assert
Assert.True(result.Success);
Assert.Equal(expirationTokens, result.ExpirationTokens);
}
[Fact]
public void CreateFactory_ProducesDelegateThatSetsPagePath()
{
// Arrange
var compilerCache = new Mock<ICompilerCache>();
compilerCache
.Setup(f => f.GetOrAdd(It.IsAny<string>(), It.IsAny<Func<RelativeFileInfo, CompilationResult>>()))
.Returns(new CompilerCacheResult(new CompilationResult(typeof(TestRazorPage)), new IChangeToken[0]));
var compilerCacheProvider = new Mock<ICompilerCacheProvider>();
compilerCacheProvider
.SetupGet(c => c.Cache)
.Returns(compilerCache.Object);
var factoryProvider = new DefaultRazorPageFactoryProvider(
Mock.Of<IRazorCompilationService>(),
compilerCacheProvider.Object);
// Act
var result = factoryProvider.CreateFactory("/file-exists");
// Assert
Assert.True(result.Success);
var actual = result.RazorPageFactory();
Assert.Equal("/file-exists", actual.Path);
}
private class TestRazorPage : RazorPage
{
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
}
}

View File

@ -1,522 +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.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class DefaultViewLocationCacheTest
{
public static IEnumerable<object[]> CacheEntryData
{
get
{
yield return new[] { new ViewLocationExpanderContext(GetActionContext(), "test", isPartial: false) };
yield return new[] { new ViewLocationExpanderContext(GetActionContext(), "test", isPartial: true) };
var areaActionContext = GetActionContext("controller2", "myarea");
yield return new[] { new ViewLocationExpanderContext(areaActionContext, "test2", isPartial: false) };
yield return new[] { new ViewLocationExpanderContext(areaActionContext, "test2", isPartial: true) };
var actionContext = GetActionContext("controller3", "area3");
var values = new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "culture", "fr" },
{ "theme", "sleek" }
};
var expanderContext = new ViewLocationExpanderContext(actionContext, "test3", isPartial: false)
{
Values = values
};
yield return new[] { expanderContext };
expanderContext = new ViewLocationExpanderContext(actionContext, "test3", isPartial: true)
{
Values = values
};
yield return new[] { expanderContext };
}
}
private static DefaultViewLocationCache.ViewLocationCacheKeyComparer CacheKeyComparer =>
DefaultViewLocationCache.ViewLocationCacheKeyComparer.Instance;
[Theory]
[MemberData(nameof(CacheEntryData))]
public void Get_ReturnsNoneResultIfItemDoesNotExist(ViewLocationExpanderContext context)
{
// Arrange
var cache = new DefaultViewLocationCache();
// Act
var result = cache.Get(context);
// Assert
Assert.Equal(result, ViewLocationCacheResult.None);
}
[Theory]
[MemberData(nameof(CacheEntryData))]
public void InvokingGetAfterSet_ReturnsCachedItem(ViewLocationExpanderContext context)
{
// Arrange
var cache = new DefaultViewLocationCache();
var value = new ViewLocationCacheResult(
Guid.NewGuid().ToString(),
new[]
{
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString()
});
// Act - 1
cache.Set(context, value);
var result = cache.Get(context);
// Assert - 1
Assert.Equal(value, result);
// Act - 2
result = cache.Get(context);
// Assert - 2
Assert.Equal(value, result);
}
[Theory]
[InlineData("View1", "View2")]
[InlineData("View1", "view1")]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfViewNamesAreDifferent(
string viewName1,
string viewName2)
{
// Arrange
var actionContext = GetActionContext();
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
actionContext,
viewName1,
isPartial: true);
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
actionContext,
viewName2,
isPartial: true);
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, false)]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfIsPartialAreDifferent(
bool isPartial1,
bool isPartial2)
{
// Arrange
var actionContext = GetActionContext();
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
actionContext,
"View1",
isPartial1);
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
actionContext,
"View1",
isPartial2);
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Theory]
[InlineData("Controller1", "Controller2")]
[InlineData("controller1", "Controller1")]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfIsControllerNamesAreDifferent(
string controller1,
string controller2)
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext(controller1),
"View1",
isPartial: false);
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext(controller2),
"View1",
isPartial: false);
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Theory]
[InlineData("area1", null)]
[InlineData("Area1", "Area2")]
[InlineData("area1", "aRea1")]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfIsAreaNamesAreDifferent(
string area1,
string area2)
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", area1),
"View1",
isPartial: false);
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", area2),
"View1",
isPartial: false);
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Fact]
public void ViewLocationCacheKeyComparer_EqualsReturnsTrueIfControllerAreaAndViewNamesAreIdentical()
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.True(result);
Assert.Equal(hash1, hash2);
}
[Fact]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfViewLocationExpanderIsNullForEitherKey()
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext1.Values = new Dictionary<string, string>
{
{ "somekey", "somevalue" }
};
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Fact]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfExpanderValueCountIsDifferent()
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext1.Values = new Dictionary<string, string>
{
{ "somekey", "somevalue" }
};
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext2.Values = new Dictionary<string, string>
{
{ "somekey", "somevalue" },
{ "somekey2", "somevalue2" },
};
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Theory]
[InlineData("key1", "key2")]
[InlineData("Key1", "key1")]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfKeysAreDifferent(
string keyName1,
string keyName2)
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext1.Values = new Dictionary<string, string>
{
{ keyName1, "somevalue" }
};
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext2.Values = new Dictionary<string, string>
{
{ keyName2, "somevalue" },
};
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
[Theory]
[InlineData("value1", null)]
[InlineData("value1", "value2")]
[InlineData("value1", "Value1")]
public void ViewLocationCacheKeyComparer_EqualsReturnsFalseIfValuesAreDifferent(
string value1,
string value2)
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext1.Values = new Dictionary<string, string>
{
{ "somekey", value1 }
};
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext2.Values = new Dictionary<string, string>
{
{ "somekey", value2 },
};
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.False(result);
Assert.NotEqual(hash1, hash2);
}
public void ViewLocationCacheKeyComparer_EqualsReturnsTrueIfValuesAreSame()
{
// Arrange
var viewLocationExpanderContext1 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext1.Values = new Dictionary<string, string>
{
{ "somekey1", "value1" },
{ "somekey2", "value2" },
};
var viewLocationExpanderContext2 = new ViewLocationExpanderContext(
GetActionContext("Controller1", "Area1"),
"View1",
isPartial: false);
viewLocationExpanderContext2.Values = new Dictionary<string, string>
{
{ "somekey2", "value2" },
{ "somekey1", "value1" },
};
// Act
var key1 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext1,
copyViewExpanderValues: false);
var key2 = DefaultViewLocationCache.GenerateKey(
viewLocationExpanderContext2,
copyViewExpanderValues: false);
var result = CacheKeyComparer.Equals(key1, key2);
var hash1 = CacheKeyComparer.GetHashCode(key1);
var hash2 = CacheKeyComparer.GetHashCode(key2);
// Assert
Assert.True(result);
Assert.Equal(hash1, hash2);
}
public static ActionContext GetActionContext(
string controller = "mycontroller",
string area = null)
{
var routeData = new RouteData();
routeData.Values["controller"] = controller;
if (area != null)
{
routeData.Values["area"] = area;
}
var actionDesciptor = new ActionDescriptor();
actionDesciptor.RouteConstraints = new List<RouteDataActionConstraint>();
return new ActionContext(new DefaultHttpContext(), routeData, actionDesciptor);
}
private static ActionContext GetActionContextWithActionDescriptor(
IDictionary<string, object> routeValues,
IDictionary<string, string> routesInActionDescriptor,
bool isAttributeRouted)
{
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
foreach (var kvp in routeValues)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
var actionDescriptor = new ActionDescriptor();
if (isAttributeRouted)
{
actionDescriptor.AttributeRouteInfo = new Routing.AttributeRouteInfo();
foreach (var kvp in routesInActionDescriptor)
{
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
}
}
else
{
actionDescriptor.RouteConstraints = new List<RouteDataActionConstraint>();
foreach (var kvp in routesInActionDescriptor)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value));
}
}
return new ActionContext(httpContext, routeData, actionDescriptor);
}
}
}

View File

@ -131,7 +131,12 @@ namespace Microsoft.AspNet.Mvc.Razor
IEnumerable<string> expectedViewLocations) IEnumerable<string> expectedViewLocations)
{ {
// Arrange // Arrange
var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(),"testView", false); var viewLocationExpanderContext = new ViewLocationExpanderContext(
new ActionContext(),
"testView",
"test-controller",
"",
false);
var languageViewLocationExpander = new LanguageViewLocationExpander(format); var languageViewLocationExpander = new LanguageViewLocationExpander(format);
viewLocationExpanderContext.Values = new Dictionary<string, string>(); viewLocationExpanderContext.Values = new Dictionary<string, string>();
viewLocationExpanderContext.Values["language"] = "en-GB"; viewLocationExpanderContext.Values["language"] = "en-GB";
@ -150,7 +155,12 @@ namespace Microsoft.AspNet.Mvc.Razor
public void ExpandViewLocations_NullContextValue(IEnumerable<string> viewLocations) public void ExpandViewLocations_NullContextValue(IEnumerable<string> viewLocations)
{ {
// Arrange // Arrange
var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(), "testView", false); var viewLocationExpanderContext = new ViewLocationExpanderContext(
new ActionContext(),
"testView",
"test-controller",
"test-area",
false);
var languageViewLocationExpander = new LanguageViewLocationExpander(); var languageViewLocationExpander = new LanguageViewLocationExpander();
viewLocationExpanderContext.Values = new Dictionary<string, string>(); viewLocationExpanderContext.Values = new Dictionary<string, string>();
@ -168,7 +178,12 @@ namespace Microsoft.AspNet.Mvc.Razor
public void ExpandViewLocations_IncorrectLocaleContextValue(IEnumerable<string> viewLocations) public void ExpandViewLocations_IncorrectLocaleContextValue(IEnumerable<string> viewLocations)
{ {
// Arrange // Arrange
var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(), "testView", false); var viewLocationExpanderContext = new ViewLocationExpanderContext(
new ActionContext(),
"testView",
"test-controller",
"test-area",
false);
var languageViewLocationExpander = new LanguageViewLocationExpander(); var languageViewLocationExpander = new LanguageViewLocationExpander();
viewLocationExpanderContext.Values = new Dictionary<string, string>(); viewLocationExpanderContext.Values = new Dictionary<string, string>();
viewLocationExpanderContext.Values["language"] = "!-invalid-culture-!"; viewLocationExpanderContext.Values["language"] = "!-invalid-culture-!";

View File

@ -1,16 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Threading;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.ViewEngines; using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.OptionsModel; using Microsoft.Extensions.OptionsModel;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -116,18 +119,24 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void FindPartialView_ReturnsRazorView_IfLookupWasSuccessful() public void FindPartialView_ReturnsRazorView_IfLookupWasSuccessful()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
var view = Mock.Of<IView>(); var viewStart1 = Mock.Of<IRazorPage>();
var viewStart2 = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>())) pageFactory
.Returns(Mock.Of<IRazorPage>()); .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), true)) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]));
.Returns(view)
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object); pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0]));
pageFactory
.Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0]));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act // Act
@ -135,9 +144,52 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert // Assert
Assert.True(result.Success); Assert.True(result.Success);
Assert.Same(view, result.View); var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
Assert.Equal("test-view", result.ViewName); Assert.Equal("test-view", result.ViewName);
viewFactory.Verify(); Assert.Empty(view.ViewStartPages);
}
[Fact]
public void FindPartialView_DoesNotExpireCachedResults_IfViewStartsExpire()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
var viewStart = Mock.Of<IRazorPage>();
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]));
pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart, new[] { changeToken }));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act - 1
var result1 = viewEngine.FindPartialView(context, "test-view");
// Assert - 1
Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page, view1.RazorPage);
Assert.Equal("test-view", result1.ViewName);
Assert.Empty(view1.ViewStartPages);
// Act - 2
cancellationTokenSource.Cancel();
var result2 = viewEngine.FindPartialView(context, "test-view");
// Assert - 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page, view2.RazorPage);
pageFactory.Verify(p => p.CreateFactory("/Views/bar/test-view.cshtml"), Times.Once());
} }
[Theory] [Theory]
@ -150,8 +202,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act & Assert // Act & Assert
ExceptionAssert.ThrowsArgumentNullOrEmpty(() => viewEngine.FindPartialView(context, partialViewName), ExceptionAssert.ThrowsArgumentNullOrEmpty(
"partialViewName"); () => viewEngine.FindPartialView(context, partialViewName),
"partialViewName");
} }
[Theory] [Theory]
@ -266,18 +319,24 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void FindView_ReturnsRazorView_IfLookupWasSuccessful() public void FindView_ReturnsRazorView_IfLookupWasSuccessful()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
var view = Mock.Of<IView>(); var viewStart1 = Mock.Of<IRazorPage>();
var viewStart2 = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>())) pageFactory
.Returns(Mock.Of<IRazorPage>()); .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false)) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]));
.Returns(view)
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object); pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0]));
pageFactory
.Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0]));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act // Act
@ -285,63 +344,66 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert // Assert
Assert.True(result.Success); Assert.True(result.Success);
Assert.Same(view, result.View); var view = Assert.IsType<RazorView>(result.View);
Assert.Equal("test-view", result.ViewName); Assert.Equal("test-view", result.ViewName);
viewFactory.Verify(); Assert.Same(page, view.RazorPage);
Assert.False(view.IsPartial);
Assert.Equal(new[] { viewStart1, viewStart2 }, view.ViewStartPages);
} }
[Fact] [Fact]
public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea() public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr")) .Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr"))
.Returns(page) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable(); .Verifiable();
var viewEngine = new OverloadedLocationViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
viewFactory.Object, GetOptionsAccessor());
GetOptionsAccessor(), viewEngine.SetLocationFormats(
GetViewLocationCache()); new[] { "fake-path1/{1}/{0}.rzr" },
new[] { "fake-area-path/{2}/{1}/{0}.rzr" });
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
viewFactory.Setup(v => v.GetView(viewEngine, page, false))
.Returns(Mock.Of<IView>());
// Act // Act
var result = viewEngine.FindView(context, "test-view"); var result = viewEngine.FindView(context, "test-view");
// Assert // Assert
pageFactory.Verify(); pageFactory.Verify();
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
} }
[Fact] [Fact]
public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea() public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory pageFactory
.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr")) .Setup(p => p.CreateFactory("fake-area-path/foo/bar/test-view2.rzr"))
.Returns(page) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable(); .Verifiable();
var viewEngine = new OverloadedLocationViewEngine( var viewEngine = new TestableRazorViewEngine(
pageFactory.Object, pageFactory.Object,
viewFactory.Object, GetOptionsAccessor());
GetOptionsAccessor(), viewEngine.SetLocationFormats(
GetViewLocationCache()); new[] { "fake-path1/{1}/{0}.rzr" },
new[] { "fake-area-path/{2}/{1}/{0}.rzr" });
var context = GetActionContext(_areaTestContext); var context = GetActionContext(_areaTestContext);
viewFactory.Setup(v => v.GetView(viewEngine, page, false))
.Returns(Mock.Of<IView>());
// Act // Act
var result = viewEngine.FindView(context, "test-view2"); var result = viewEngine.FindView(context, "test-view2");
// Assert // Assert
pageFactory.Verify(); pageFactory.Verify();
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
} }
[Theory] [Theory]
@ -351,46 +413,49 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
IEnumerable<string> expectedSeeds) IEnumerable<string> expectedSeeds)
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml")) pageFactory
.Returns(Mock.Of<IRazorPage>()) .Setup(p => p.CreateFactory("test-string/bar.cshtml"))
.Verifiable(); .Returns(new RazorPageFactoryResult(() => Mock.Of<IRazorPage>(), new IChangeToken[0]))
.Verifiable();
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), It.IsAny<bool>()))
.Returns(Mock.Of<IView>());
var expander1Result = new[] { "some-seed" }; var expander1Result = new[] { "some-seed" };
var expander1 = new Mock<IViewLocationExpander>(); var expander1 = new Mock<IViewLocationExpander>();
expander1.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>())) expander1
.Callback((ViewLocationExpanderContext c) => .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
{ .Callback((ViewLocationExpanderContext c) =>
Assert.NotNull(c.ActionContext); {
c.Values["expander-key"] = expander1.ToString(); Assert.NotNull(c.ActionContext);
}) c.Values["expander-key"] = expander1.ToString();
.Verifiable(); })
expander1.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), .Verifiable();
It.IsAny<IEnumerable<string>>())) expander1
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) => .Setup(e => e.ExpandViewLocations(
{ It.IsAny<ViewLocationExpanderContext>(),
Assert.NotNull(c.ActionContext); It.IsAny<IEnumerable<string>>()))
Assert.Equal(expectedSeeds, seeds); .Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
}) {
.Returns(expander1Result) Assert.NotNull(c.ActionContext);
.Verifiable(); Assert.Equal(expectedSeeds, seeds);
})
.Returns(expander1Result)
.Verifiable();
var expander2 = new Mock<IViewLocationExpander>(); var expander2 = new Mock<IViewLocationExpander>();
expander2.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), expander2
It.IsAny<IEnumerable<string>>())) .Setup(e => e.ExpandViewLocations(
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) => It.IsAny<ViewLocationExpanderContext>(),
{ It.IsAny<IEnumerable<string>>()))
Assert.Equal(expander1Result, seeds); .Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
}) {
.Returns(new[] { "test-string/{1}.cshtml" }) Assert.Equal(expander1Result, seeds);
.Verifiable(); })
.Returns(new[] { "test-string/{1}.cshtml" })
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, var viewEngine = CreateViewEngine(
new[] { expander1.Object, expander2.Object }); pageFactory.Object,
new[] { expander1.Object, expander2.Object });
var context = GetActionContext(routeValues); var context = GetActionContext(routeValues);
// Act // Act
@ -408,130 +473,152 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void FindView_CachesValuesIfViewWasFound() public void FindView_CachesValuesIfViewWasFound()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var page = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml")) var pageFactory = new Mock<IRazorPageFactoryProvider>();
.Verifiable(); pageFactory
pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml")) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(Mock.Of<IRazorPage>()) .Returns(new RazorPageFactoryResult(new IChangeToken[0]))
.Verifiable();
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>());
var cache = GetViewLocationCache();
var cacheMock = Mock.Get(cache);
cacheMock.Setup(c => c.Set(It.IsAny<ViewLocationExpanderContext>(), It.IsAny<ViewLocationCacheResult>()))
.Callback((ViewLocationExpanderContext _, ViewLocationCacheResult cacheResult) =>
{
Assert.Equal("/Views/Shared/baz.cshtml", cacheResult.ViewLocation);
Assert.Equal(new[] { "/Views/bar/baz.cshtml" }, cacheResult.SearchedLocations);
})
.Verifiable(); .Verifiable();
pageFactory
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, cache: cache); var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act // Act 1
var result = viewEngine.FindView(context, "baz"); var result1 = viewEngine.FindView(context, "baz");
// Assert // Assert 1
Assert.True(result.Success); Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page, view1.RazorPage);
pageFactory.Verify();
// Act 2
pageFactory
.Setup(p => p.CreateFactory(It.IsAny<string>()))
.Throws(new Exception("Shouldn't be called"));
var result2 = viewEngine.FindView(context, "baz");
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page, view2.RazorPage);
pageFactory.Verify(); pageFactory.Verify();
cacheMock.Verify();
} }
[Fact] [Fact]
public void FindView_UsesCachedValueIfViewWasFound() public void FindView_InvokesPageFactoryIfChangeTokenExpired()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(MockBehavior.Strict); var page1 = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance("some-view-location")) var page2 = Mock.Of<IRazorPage>();
.Returns(Mock.Of<IRazorPage>()) var sequence = new MockSequence();
.Verifiable(); var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
var viewFactory = new Mock<IRazorViewFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false)) pageFactory
.Returns(Mock.Of<IView>()); .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(new[] { changeToken }));
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0]))
.Verifiable();
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0]));
var expander = new Mock<IViewLocationExpander>(MockBehavior.Strict); var viewEngine = CreateViewEngine(pageFactory.Object);
expander.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Verifiable();
var cacheMock = new Mock<IViewLocationCache>();
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
.Returns(new ViewLocationCacheResult("some-view-location", Enumerable.Empty<string>()))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object,
viewFactory.Object,
new[] { expander.Object },
cacheMock.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act // Act 1
var result = viewEngine.FindView(context, "baz"); var result1 = viewEngine.FindView(context, "baz");
// Assert // Assert 1
Assert.True(result.Success); Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page1, view1.RazorPage);
// Act 2
cancellationTokenSource.Cancel();
var result2 = viewEngine.FindView(context, "baz");
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page2, view2.RazorPage);
pageFactory.Verify(); pageFactory.Verify();
cacheMock.Verify();
expander.Verify();
} }
[Fact] [Fact]
public void FindView_LooksForViewsIfCachedViewDoesNotExist() public void FindView_InvokesPageFactoryIfViewStartExpirationTokensHaveExpired()
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var page1 = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance("expired-location")) var page2 = Mock.Of<IRazorPage>();
.Returns((IRazorPage)null) var viewStart = Mock.Of<IRazorPage>();
.Verifiable(); var sequence = new MockSequence();
pageFactory.Setup(p => p.CreateInstance("some-view-location")) var cancellationTokenSource = new CancellationTokenSource();
.Returns(Mock.Of<IRazorPage>()) var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
.Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false)) pageFactory
.Returns(Mock.Of<IView>()); .InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0]));
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(new[] { changeToken }))
.Verifiable();
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0]));
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(new RazorPageFactoryResult(() => viewStart, new IChangeToken[0]));
var cacheMock = new Mock<IViewLocationCache>(); var viewEngine = CreateViewEngine(pageFactory.Object);
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
.Returns(new ViewLocationCacheResult("expired-location", Enumerable.Empty<string>()));
var expander = new Mock<IViewLocationExpander>();
expander.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Verifiable();
var expanderResult = new[] { "some-view-location" };
expander.Setup(v => v.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()))
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seed) => expanderResult)
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object,
viewFactory.Object,
expanders: new[] { expander.Object },
cache: cacheMock.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act // Act 1
var result = viewEngine.FindView(context, "baz"); var result1 = viewEngine.FindView(context, "baz");
// Assert // Assert 1
Assert.True(result.Success); Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page1, view1.RazorPage);
Assert.Empty(view1.ViewStartPages);
// Act 2
cancellationTokenSource.Cancel();
var result2 = viewEngine.FindView(context, "baz");
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page2, view2.RazorPage);
var actualViewStart = Assert.Single(view2.ViewStartPages);
Assert.Equal(viewStart, actualViewStart);
pageFactory.Verify(); pageFactory.Verify();
cacheMock.Verify();
expander.Verify();
} }
// This test validates an important perf scenario of RazorViewEngine not constructing // This test validates an important perf scenario of RazorViewEngine not constructing
// multiple strings for views that do not exist in the file system on a per-request basis. // multiple strings for views that do not exist in the file system on a per-request basis.
[Fact] [Fact]
public void FindView_DoesNotInvokeExpandViewLocations_IfCacheEntryMatchesButViewIsNotFound() public void FindView_DoesNotInvokeViewLocationExpanders_IfChangeTokenHasNotExpired()
{ {
// Arrange // Arrange
var pageFactory = Mock.Of<IRazorPageFactory>(); var pageFactory = Mock.Of<IRazorPageFactoryProvider>();
var viewFactory = Mock.Of<IRazorViewFactory>();
var cache = new DefaultViewLocationCache();
var expander = new Mock<IViewLocationExpander>(); var expander = new Mock<IViewLocationExpander>();
var expandedLocations = new[] var expandedLocations = new[]
{ {
@ -548,16 +635,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
expander expander
.Setup(v => v.ExpandViewLocations( .Setup(v => v.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>())) It.IsAny<ViewLocationExpanderContext>(),
.Returns((ViewLocationExpanderContext c, IEnumerable<string> viewLocations) => expandedLocations) It.IsAny<IEnumerable<string>>()))
.Returns(expandedLocations)
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine( var viewEngine = CreateViewEngine(
pageFactory, pageFactory,
viewFactory, expanders: new[] { expander.Object });
expanders: new[] { expander.Object },
cache: cache);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act - 1 // Act - 1
@ -582,6 +667,70 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Times.Once()); Times.Once());
} }
[Fact]
public void FindView_InvokesViewLocationExpanders_IfChangeTokenExpires()
{
// Arrange
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("viewlocation3"))
.Returns(new RazorPageFactoryResult(new[] { changeToken }));
var expander = new Mock<IViewLocationExpander>();
var expandedLocations = new[]
{
"viewlocation1",
"viewlocation2",
"viewlocation3",
};
expander
.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext expanderContext) =>
{
expanderContext.Values["somekey"] = "somevalue";
})
.Verifiable();
expander
.Setup(v => v.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(expandedLocations)
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory.Object,
expanders: new[] { expander.Object });
var context = GetActionContext(_controllerTestContext);
// Act - 1
var result = viewEngine.FindView(context, "MyView");
// Assert - 1
Assert.False(result.Success);
Assert.Equal(expandedLocations, result.SearchedLocations);
expander.Verify();
// Act - 2
pageFactory
.Setup(p => p.CreateFactory("viewlocation3"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]));
cancellationTokenSource.Cancel();
result = viewEngine.FindView(context, "MyView");
// Assert - 2
Assert.True(result.Success);
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
expander.Verify(
v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()),
Times.Exactly(2));
expander.Verify(
v => v.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()),
Times.Exactly(2));
}
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
@ -604,36 +753,39 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateInstance("expanded-path/bar-layout")) pageFactory
.Returns(page) .Setup(p => p.CreateFactory("expanded-path/bar-layout"))
.Verifiable(); .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(MockBehavior.Strict);
var expander = new Mock<IViewLocationExpander>(); var expander = new Mock<IViewLocationExpander>();
expander.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>())) expander
.Callback((ViewLocationExpanderContext c) => .Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
{ .Callback((ViewLocationExpanderContext c) =>
Assert.NotNull(c.ActionContext); {
c.Values["expander-key"] = expander.ToString(); Assert.NotNull(c.ActionContext);
}) c.Values["expander-key"] = expander.ToString();
.Verifiable(); })
expander.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), .Verifiable();
It.IsAny<IEnumerable<string>>())) expander
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) => .Setup(e => e.ExpandViewLocations(
{ It.IsAny<ViewLocationExpanderContext>(),
Assert.NotNull(c.ActionContext); It.IsAny<IEnumerable<string>>()))
Assert.Equal(expectedSeeds, seeds); .Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
Assert.Equal(expander.ToString(), c.Values["expander-key"]); Assert.Equal(expander.ToString(), c.Values["expander-key"]);
return new[] { "expanded-path/bar-{0}" }; return new[] { "expanded-path/bar-{0}" };
}) })
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, var viewEngine = CreateViewEngine(
new[] { expander.Object }); pageFactory.Object,
new[] { expander.Object });
var context = GetActionContext(routeValues); var context = GetActionContext(routeValues);
// Act // Act
@ -686,10 +838,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}; };
var page = new Mock<IRazorPage>(MockBehavior.Strict).Object; var page = new Mock<IRazorPage>(MockBehavior.Strict).Object;
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateInstance("/Views/Foo/details.cshtml")) pageFactory
.Returns(page) .Setup(p => p.CreateFactory("/Views/Foo/details.cshtml"))
.Verifiable(); .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object); var viewEngine = CreateViewEngine(pageFactory.Object);
var routesInActionDescriptor = new Dictionary<string, string>() var routesInActionDescriptor = new Dictionary<string, string>()
@ -1092,23 +1245,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
} }
} }
private RazorViewEngine CreateViewEngine( private TestableRazorViewEngine CreateViewEngine(
IRazorPageFactory pageFactory = null, IRazorPageFactoryProvider pageFactory = null,
IRazorViewFactory viewFactory = null, IEnumerable<IViewLocationExpander> expanders = null)
IEnumerable<IViewLocationExpander> expanders = null,
IViewLocationCache cache = null)
{ {
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>(); pageFactory = pageFactory ?? Mock.Of<IRazorPageFactoryProvider>();
viewFactory = viewFactory ?? Mock.Of<IRazorViewFactory>(); return new TestableRazorViewEngine(
pageFactory,
cache = cache ?? GetViewLocationCache(); GetOptionsAccessor(expanders));
var viewEngine = new RazorViewEngine(pageFactory,
viewFactory,
GetOptionsAccessor(expanders),
cache);
return viewEngine;
} }
private static IOptions<RazorViewEngineOptions> GetOptionsAccessor( private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
@ -1129,15 +1273,6 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
return optionsAccessor.Object; return optionsAccessor.Object;
} }
private static IViewLocationCache GetViewLocationCache()
{
var cacheMock = new Mock<IViewLocationCache>();
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
.Returns<string>(null);
return cacheMock.Object;
}
private static ActionContext GetActionContext(IDictionary<string, object> routeValues) private static ActionContext GetActionContext(IDictionary<string, object> routeValues)
{ {
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
@ -1185,32 +1320,33 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
return new ActionContext(httpContext, routeData, actionDescriptor); return new ActionContext(httpContext, routeData, actionDescriptor);
} }
private class OverloadedLocationViewEngine : RazorViewEngine private class TestableRazorViewEngine : RazorViewEngine
{ {
public OverloadedLocationViewEngine( private IEnumerable<string> _viewLocationFormats;
IRazorPageFactory pageFactory, private IEnumerable<string> _areaViewLocationFormats;
IRazorViewFactory viewFactory,
IOptions<RazorViewEngineOptions> optionsAccessor, public TestableRazorViewEngine(
IViewLocationCache cache) IRazorPageFactoryProvider pageFactory,
: base(pageFactory, viewFactory, optionsAccessor, cache) IOptions<RazorViewEngineOptions> optionsAccessor)
: base(pageFactory, Mock.Of<IRazorPageActivator>(), new HtmlTestEncoder(), optionsAccessor)
{ {
} }
public override IEnumerable<string> ViewLocationFormats public void SetLocationFormats(
IEnumerable<string> viewLocationFormats,
IEnumerable<string> areaViewLocationFormats)
{ {
get _viewLocationFormats = viewLocationFormats;
{ _areaViewLocationFormats = areaViewLocationFormats;
return new[] { "fake-path1/{1}/{0}.rzr" };
}
} }
public override IEnumerable<string> AreaViewLocationFormats public override IEnumerable<string> ViewLocationFormats =>
{ _viewLocationFormats != null ? _viewLocationFormats : base.ViewLocationFormats;
get
{ public override IEnumerable<string> AreaViewLocationFormats =>
return new[] { "fake-area-path/{2}/{1}/{0}.rzr" }; _areaViewLocationFormats != null ? _areaViewLocationFormats : base.AreaViewLocationFormats;
}
} public IMemoryCache ViewLookupCachePublic => ViewLookupCache;
} }
} }
} }

View File

@ -1,55 +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.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorViewFactoryTest
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetView_SetsIsPartial(bool isPartial)
{
// Arrange
var factory = new RazorViewFactory(
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(),
new HtmlTestEncoder());
var page = Mock.Of<IRazorPage>();
var viewEngine = Mock.Of<IRazorViewEngine>();
// Act
var view = factory.GetView(viewEngine, page, isPartial);
// Assert
var razorView = Assert.IsType<RazorView>(view);
Assert.Same(page, razorView.RazorPage);
Assert.Equal(razorView.IsPartial, isPartial);
}
[Fact]
public void GetView_SetsRazorPage()
{
// Arrange
var factory = new RazorViewFactory(
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(),
new HtmlTestEncoder());
var page = Mock.Of<IRazorPage>();
var viewEngine = Mock.Of<IRazorViewEngine>();
// Act
var view = factory.GetView(viewEngine, page, isPartial: false);
// Assert
Assert.NotNull(view);
var razorView = Assert.IsType<RazorView>(view);
Assert.Same(razorView.RazorPage, page);
}
}
}

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
@ -14,6 +13,7 @@ using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -39,12 +39,13 @@ namespace Microsoft.AspNet.Mvc.Razor
v.HtmlEncoder = new HtmlTestEncoder(); v.HtmlEncoder = new HtmlTestEncoder();
v.Write("Hello world"); v.Write("Hello world");
}); });
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(), Mock.Of<IRazorPageActivator>(),
page, new IRazorPage[0],
new HtmlTestEncoder(), page,
isPartial: true); new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
var expected = viewContext.Writer; var expected = viewContext.Writer;
@ -68,12 +69,13 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Same(viewData, v.ViewContext.ViewData); Assert.Same(viewData, v.ViewContext.ViewData);
}); });
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
activator.Object, Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(), activator.Object,
page, new IRazorPage[0],
new HtmlTestEncoder(), page,
isPartial: true); new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
var expectedWriter = viewContext.Writer; var expectedWriter = viewContext.Writer;
@ -132,12 +134,13 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewEngine = new Mock<IRazorViewEngine>(); var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath)) viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(new RazorPageResult(LayoutPath, layout)); .Returns(new RazorPageResult(LayoutPath, layout));
var view = new RazorView(viewEngine.Object, var view = new RazorView(
activator, viewEngine.Object,
CreateViewStartProvider(viewStart), activator,
page, new[] { viewStart },
new HtmlTestEncoder(), page,
isPartial: false); new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
var expectedWriter = viewContext.Writer; var expectedWriter = viewContext.Writer;
@ -157,12 +160,13 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
activator.Object, Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(), activator.Object,
page, new IRazorPage[0],
new HtmlTestEncoder(), page,
isPartial: true); new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
// Act // Act
@ -193,29 +197,28 @@ namespace Microsoft.AspNet.Mvc.Razor
v.Write("layout-content" + Environment.NewLine); v.Write("layout-content" + Environment.NewLine);
v.RenderBodyPublic(); v.RenderBodyPublic();
}); });
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath)) pageFactory
.Returns(layout); .Setup(p => p.CreateFactory(LayoutPath))
.Returns(new RazorPageFactoryResult(() => layout, new IChangeToken[0]));
var viewEngine = new Mock<IRazorViewEngine>(); var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath)) viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(new RazorPageResult(LayoutPath, layout)); .Returns(new RazorPageResult(LayoutPath, layout));
var viewStartProvider = CreateViewStartProvider(); var view = new RazorView(
var view = new RazorView(viewEngine.Object, viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
viewStartProvider, new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: true); isPartial: true);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
// Act // Act
await view.RenderAsync(viewContext); await view.RenderAsync(viewContext);
// Assert // Assert
Mock.Get(viewStartProvider)
.Verify(v => v.GetViewStartPages(It.IsAny<string>()), Times.Never());
Assert.Equal(expected, viewContext.Writer.ToString()); Assert.Equal(expected, viewContext.Writer.ToString());
} }
@ -228,12 +231,13 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
actual = v.Output; actual = v.Output;
}); });
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(), Mock.Of<IRazorPageActivator>(),
page, new IRazorPage[0],
new HtmlTestEncoder(), page,
isPartial: false); new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
var original = viewContext.Writer; var original = viewContext.Writer;
@ -253,12 +257,13 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
v.WriteLiteral("Hello world"); v.WriteLiteral("Hello world");
}); });
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(), Mock.Of<IRazorPageActivator>(),
page, new IRazorPage[0],
new HtmlTestEncoder(), page,
isPartial: false); new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
var original = viewContext.Writer; var original = viewContext.Writer;
@ -280,12 +285,13 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
activator.Object, Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(), activator.Object,
page, new IRazorPage[0],
new HtmlTestEncoder(), page,
isPartial: false); new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
// Act // Act
@ -323,12 +329,13 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable(); .Verifiable();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>())) activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable(); .Verifiable();
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(
activator.Object, Mock.Of<IRazorViewEngine>(),
CreateViewStartProvider(viewStart1, viewStart2), activator.Object,
page, new[] { viewStart1, viewStart2 },
new HtmlTestEncoder(), page,
isPartial: false); new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
// Act // Act
@ -358,7 +365,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>(); var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -422,7 +429,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
activator.Object, activator.Object,
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -464,7 +471,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -527,7 +534,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -587,7 +594,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -647,7 +654,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -712,7 +719,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -744,7 +751,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -808,7 +815,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -844,7 +851,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -889,7 +896,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -956,7 +963,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1009,7 +1016,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1059,7 +1066,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1089,7 +1096,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1134,7 +1141,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1204,13 +1211,9 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewEngine = new Mock<IRazorViewEngine>(); var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "Layout")) viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "Layout"))
.Returns(new RazorPageResult("Layout", layout)); .Returns(new RazorPageResult("Layout", layout));
var viewStartProvider = new Mock<IViewStartProvider>();
viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny<string>()))
.Returns(Enumerable.Empty<IRazorPage>())
.Verifiable();
var view = new RazorView(viewEngine.Object, var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
viewStartProvider.Object, new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1222,7 +1225,6 @@ namespace Microsoft.AspNet.Mvc.Razor
// Assert // Assert
feature.Verify(); feature.Verify();
viewStartProvider.Verify();
Assert.True(layoutExecuted); Assert.True(layoutExecuted);
} }
@ -1256,7 +1258,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: true); isPartial: true);
@ -1288,7 +1290,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial); isPartial);
@ -1328,7 +1330,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine, var view = new RazorView(viewEngine,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart1, viewStart2), new[] { viewStart1, viewStart2 },
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1364,12 +1366,13 @@ namespace Microsoft.AspNet.Mvc.Razor
}); });
var viewEngine = Mock.Of<IRazorViewEngine>(); var viewEngine = Mock.Of<IRazorViewEngine>();
var view = new RazorView(viewEngine, var view = new RazorView(
Mock.Of<IRazorPageActivator>(), viewEngine,
CreateViewStartProvider(viewStart1, viewStart2), Mock.Of<IRazorPageActivator>(),
page, new[] { viewStart1, viewStart2 },
new HtmlTestEncoder(), page,
isPartial: false); new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view); var viewContext = CreateViewContext(view);
// Act // Act
@ -1402,13 +1405,14 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderBodyPublic(); v.RenderBodyPublic();
}); });
var viewEngine = new Mock<IRazorViewEngine>(); var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "/Layout.cshtml")) viewEngine
.Returns(new RazorPageResult("Layout", layout)); .Setup(p => p.FindPage(It.IsAny<ActionContext>(), "/Layout.cshtml"))
.Returns(new RazorPageResult("Layout", layout));
var view = new RazorView( var view = new RazorView(
viewEngine.Object, viewEngine.Object,
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart), new[] { viewStart },
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: false); isPartial: false);
@ -1434,7 +1438,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}); });
var view = new RazorView(Mock.Of<IRazorViewEngine>(), var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(), Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(), new IRazorPage[0],
page, page,
new HtmlTestEncoder(), new HtmlTestEncoder(),
isPartial: true); isPartial: true);
@ -1447,6 +1451,48 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.True(isPartialPage.Value); Assert.True(isPartialPage.Value);
} }
[Fact]
public async Task RenderAsync_RendersViewStartsInOrderInWhichTheyAreSpecified()
{
// Arrange
var expected = string.Join(
Environment.NewLine,
new[]
{
"ViewStart1",
"ViewStart2",
"Page",
});
var page = new TestableRazorPage(v =>
{
v.WriteLiteral("Page");
});
var viewStart1 = new TestableRazorPage(v =>
{
v.WriteLiteral("ViewStart1" + Environment.NewLine);
});
var viewStart2 = new TestableRazorPage(v =>
{
v.WriteLiteral("ViewStart2" + Environment.NewLine);
});
var viewEngine = Mock.Of<IRazorViewEngine>();
var view = new RazorView(
viewEngine,
Mock.Of<IRazorPageActivator>(),
new[] { viewStart1, viewStart2 },
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
// Act
await view.RenderAsync(viewContext);
// Assert
Assert.Equal(expected, viewContext.Writer.ToString());
}
private static TextWriter CreateBufferedWriter() private static TextWriter CreateBufferedWriter()
{ {
var mockWriter = new Mock<TextWriter>(); var mockWriter = new Mock<TextWriter>();
@ -1469,17 +1515,6 @@ namespace Microsoft.AspNet.Mvc.Razor
new HtmlHelperOptions()); new HtmlHelperOptions());
} }
private static IViewStartProvider CreateViewStartProvider(params IRazorPage[] viewStartPages)
{
viewStartPages = viewStartPages ?? new IRazorPage[0];
var viewStartProvider = new Mock<IViewStartProvider>();
viewStartProvider
.Setup(v => v.GetViewStartPages(It.IsAny<string>()))
.Returns(viewStartPages);
return viewStartProvider.Object;
}
private class TestableRazorPage : RazorPage private class TestableRazorPage : RazorPage
{ {
private readonly Action<TestableRazorPage> _executeAction; private readonly Action<TestableRazorPage> _executeAction;