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>
/// Represents the result of compilation.
/// </summary>
public class CompilationResult
public struct CompilationResult
{
/// <summary>
/// Creates a new instance of <see cref="CompilationResult"/>.
/// </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.
/// Initializes a new instance of <see cref="CompilationResult"/> for a successful compilation.
/// </summary>
/// <param name="type">The compiled type.</param>
/// <returns>A <see cref="CompilationResult"/> instance for a successful compilation.</returns>
public static CompilationResult Successful(Type type)
public CompilationResult(Type type)
{
if (type == null)
{
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 Microsoft.AspNet.FileProviders;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
@ -56,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
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);
}
}
@ -95,33 +96,29 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile)
{
CompilerCacheResult cacheResult;
var fileInfo = _fileProvider.GetFileInfo(normalizedPath);
MemoryCacheEntryOptions cacheEntryOptions;
CompilerCacheResult cacheResultToCache;
CompilerCacheResult cacheResult;
if (!fileInfo.Exists)
{
cacheResultToCache = CompilerCacheResult.FileNotFound;
cacheResult = CompilerCacheResult.FileNotFound;
var expirationToken = _fileProvider.Watch(normalizedPath);
cacheResult = new CompilerCacheResult(new[] { expirationToken });
cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.AddExpirationToken(_fileProvider.Watch(normalizedPath));
cacheEntryOptions.AddExpirationToken(expirationToken);
}
else
{
var relativeFileInfo = new RelativeFileInfo(fileInfo, normalizedPath);
var compilationResult = compile(relativeFileInfo).EnsureSuccessful();
var compilationResult = compile(relativeFileInfo);
compilationResult.EnsureSuccessful();
cacheEntryOptions = GetMemoryCacheEntryOptions(normalizedPath);
// By default the CompilationResult returned by IRoslynCompiler is an instance of
// UncachedCompilationResult. This type has the generated code as a string property and do not want
// to cache it. We'll instead cache the unwrapped result.
cacheResultToCache = new CompilerCacheResult(
CompilationResult.Successful(compilationResult.CompiledType));
cacheResult = new CompilerCacheResult(compilationResult);
cacheResult = new CompilerCacheResult(
compilationResult,
cacheEntryOptions.ExpirationTokens);
}
_cache.Set(normalizedPath, cacheResultToCache, cacheEntryOptions);
_cache.Set(normalizedPath, cacheResult, cacheEntryOptions);
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.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
/// <summary>
/// Result of <see cref="ICompilerCache"/>.
/// </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>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="CompilationResult"/>.
/// <see cref="Compilation.CompilationResult"/>.
/// </summary>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/> </param>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
public CompilerCacheResult(CompilationResult compilationResult)
: this(compilationResult, new IChangeToken[0])
{
if (compilationResult == null)
{
throw new ArgumentNullException(nameof(compilationResult));
}
CompilationResult = compilationResult;
}
/// <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>
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>
/// The <see cref="Compilation.CompilationResult"/>.
/// </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; }
/// <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);
}
return CompilationResult.Failed(failures);
return new CompilationResult(failures);
}
private DiagnosticMessage CreateDiagnosticMessage(RazorError error, string filePath)

View File

@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var type = assembly.GetExportedTypes()
.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);
}
return CompilationResult.Failed(failures);
return new CompilationResult(failures);
}
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
{
/// <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.
/// </summary>
public class VirtualPathRazorPageFactory : IRazorPageFactory
public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider
{
/// <remarks>
/// This delegate holds on to an instance of <see cref="IRazorCompilationService"/>.
@ -20,11 +20,11 @@ namespace Microsoft.AspNet.Mvc.Razor
private ICompilerCache _compilerCache;
/// <summary>
/// Initializes a new instance of <see cref="VirtualPathRazorPageFactory"/>.
/// Initializes a new instance of <see cref="DefaultRazorPageFactoryProvider"/>.
/// </summary>
/// <param name="razorCompilationService">The <see cref="IRazorCompilationService"/>.</param>
/// <param name="compilerCacheProvider">The <see cref="ICompilerCacheProvider"/>.</param>
public VirtualPathRazorPageFactory(
public DefaultRazorPageFactoryProvider(
IRazorCompilationService razorCompilationService,
ICompilerCacheProvider compilerCacheProvider)
{
@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <inheritdoc />
public IRazorPage CreateInstance(string relativePath)
public RazorPageFactoryResult CreateFactory(string relativePath)
{
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.
relativePath = relativePath.Substring(1);
}
var result = CompilerCache.GetOrAdd(relativePath, _compileDelegate);
if (result == CompilerCacheResult.FileNotFound)
if (result.Success)
{
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);
page.Path = relativePath;
return page;
/// <summary>
/// Creates a factory for <see cref="IRazorPage"/>.
/// </summary>
/// <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>();
// Caches view locations that are valid for the lifetime of the application.
services.TryAddSingleton<IViewLocationCache, DefaultViewLocationCache>();
services.TryAdd(ServiceDescriptor.Singleton<IChunkTreeCache>(serviceProvider =>
{
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
// creating the singleton RazorViewEngine instance.
services.TryAddTransient<IRazorViewFactory, RazorViewFactory>();
services.TryAddTransient<IRazorPageFactory, VirtualPathRazorPageFactory>();
services.TryAddTransient<IRazorPageFactoryProvider, DefaultRazorPageFactoryProvider>();
services.TryAddTransient<IRazorCompilationService, RazorCompilationService>();
services.TryAddTransient<IViewStartProvider, ViewStartProvider>();
services.TryAddTransient<IMvcRazorHost, MvcRazorHost>();
// 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>
/// Defines methods that are used for creating <see cref="IRazorPage"/> instances at a given path.
/// </summary>
public interface IRazorPageFactory
public interface IRazorPageFactoryProvider
{
/// <summary>
/// Creates a <see cref="IRazorPage"/> for the specified path.
/// Creates a <see cref="IRazorPage"/> factory for the specified path.
/// </summary>
/// <param name="relativePath">The path to locate the page.</param>
/// <returns>The IRazorPage instance if it exists, null otherwise.</returns>
IRazorPage CreateInstance(string relativePath);
/// <returns>The <see cref="RazorPageFactoryResult"/> instance.</returns>
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 GeneratePrecompiledAssembly(syntaxTrees.Where(tree => tree != null),
razorFiles.Where(file => file != null));
return GeneratePrecompiledAssembly(
syntaxTrees.Where(tree => tree != null),
razorFiles.Where(file => file != null));
}
protected virtual RazorFileInfoCollection GeneratePrecompiledAssembly(
@ -174,13 +175,15 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
var references = CompileContext.Compilation.References
.Concat(new[] { applicationReference });
var preCompilationOptions = CompilationSettings.CompilationOptions
.WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
var preCompilationOptions = CompilationSettings
.CompilationOptions
.WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
var compilation = CSharpCompilation.Create(assemblyResourceName,
options: preCompilationOptions,
syntaxTrees: syntaxTrees,
references: references);
var compilation = CSharpCompilation.Create(
assemblyResourceName,
options: preCompilationOptions,
syntaxTrees: syntaxTrees,
references: references);
var generateSymbols = GenerateSymbols && SymbolsUtility.SupportsSymbolsGeneration();
// 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)
{
var syntaxTree = SyntaxTreeGenerator.Generate(results.GeneratedCode,
fileInfo.FileInfo.PhysicalPath,
CompilationSettings);
var syntaxTree = SyntaxTreeGenerator.Generate(
results.GeneratedCode,
fileInfo.FileInfo.PhysicalPath,
CompilationSettings);
var fullTypeName = results.GetMainClassName(host, syntaxTree);
if (fullTypeName != null)
@ -297,9 +301,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
}
else
{
var diagnostics = results.ParserErrors
.Select(error => error.ToDiagnostics(fileInfo.FileInfo.PhysicalPath))
.ToList();
var diagnostics = results
.ParserErrors
.Select(error => error.ToDiagnostics(fileInfo.FileInfo.PhysicalPath))
.ToList();
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
{
/// <summary>
/// Represents the results of locating a <see cref="IRazorPage"/>.
/// Result of locating a <see cref="IRazorPage"/>.
/// </summary>
public class RazorPageResult
public struct RazorPageResult
{
/// <summary>
/// Initializes a new instance of <see cref="RazorPageResult"/> for a successful discovery.
@ -30,6 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor
Name = name;
Page = page;
SearchedLocations = null;
}
/// <summary>
@ -50,6 +51,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
Name = name;
Page = null;
SearchedLocations = searchedLocations;
}

View File

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

View File

@ -5,10 +5,12 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.OptionsModel;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Mvc.Razor
{
@ -22,41 +24,34 @@ namespace Microsoft.AspNet.Mvc.Razor
public class RazorViewEngine : IRazorViewEngine
{
private const string ViewExtension = ".cshtml";
internal const string ControllerKey = "controller";
internal const string AreaKey = "area";
private const string ControllerKey = "controller";
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[]
{
"/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 IRazorPageFactoryProvider _pageFactory;
private readonly IList<IViewLocationExpander> _viewLocationExpanders;
private readonly IViewLocationCache _viewLocationCache;
private readonly IRazorPageActivator _pageActivator;
private readonly HtmlEncoder _htmlEncoder;
/// <summary>
/// Initializes a new instance of the <see cref="RazorViewEngine" /> class.
/// Initializes a new instance of the <see cref="RazorViewEngine" />.
/// </summary>
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param>
public RazorViewEngine(
IRazorPageFactory pageFactory,
IRazorViewFactory viewFactory,
IOptions<RazorViewEngineOptions> optionsAccessor,
IViewLocationCache viewLocationCache)
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor)
{
_pageFactory = pageFactory;
_viewFactory = viewFactory;
_pageActivator = pageActivator;
_viewLocationExpanders = optionsAccessor.Value.ViewLocationExpanders;
_viewLocationCache = viewLocationCache;
_htmlEncoder = htmlEncoder;
ViewLookupCache = new MemoryCache(new MemoryCacheOptions
{
CompactOnMemoryPressure = false
});
}
/// <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
/// <c>/Views/Home/Test.cshtml</c>. Locations such as <c>/views/home/test.cshtml</c> would not be discovered
/// </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>
/// 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
/// <c>/Views/Home/Test.cshtml</c>. Locations such as <c>/views/home/test.cshtml</c> would not be discovered
/// </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 />
public ViewEngineResult FindView(
ActionContext context,
string viewName)
public ViewEngineResult FindView(ActionContext context, string viewName)
{
if (context == null)
{
@ -112,14 +113,12 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
}
var pageResult = GetRazorPageResult(context, viewName, isPartial: false);
return CreateViewEngineResult(pageResult, _viewFactory, isPartial: false);
var pageResult = GetViewLocationCacheResult(context, viewName, isPartial: false);
return CreateViewEngineResult(pageResult, viewName, isPartial: false);
}
/// <inheritdoc />
public ViewEngineResult FindPartialView(
ActionContext context,
string partialViewName)
public ViewEngineResult FindPartialView(ActionContext context, string partialViewName)
{
if (context == null)
{
@ -131,14 +130,12 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(partialViewName));
}
var pageResult = GetRazorPageResult(context, partialViewName, isPartial: true);
return CreateViewEngineResult(pageResult, _viewFactory, isPartial: true);
var pageResult = GetViewLocationCacheResult(context, partialViewName, isPartial: true);
return CreateViewEngineResult(pageResult, partialViewName, isPartial: true);
}
/// <inheritdoc />
public RazorPageResult FindPage(
ActionContext context,
string pageName)
public RazorPageResult FindPage(ActionContext context, string pageName)
{
if (context == null)
{
@ -150,7 +147,16 @@ namespace Microsoft.AspNet.Mvc.Razor
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>
@ -166,9 +172,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <see cref="Abstractions.ActionDescriptor.RouteConstraints"/> for traditional routes to get route values
/// produces consistently cased results.
/// </remarks>
public static string GetNormalizedRouteValue(
ActionContext context,
string key)
public static string GetNormalizedRouteValue(ActionContext context, string key)
{
if (context == null)
{
@ -228,26 +232,14 @@ namespace Microsoft.AspNet.Mvc.Razor
return stringRouteValue;
}
private RazorPageResult GetRazorPageResult(
private ViewLocationCacheResult GetViewLocationCacheResult(
ActionContext context,
string pageName,
bool isPartial)
{
if (IsApplicationRelativePath(pageName))
{
var applicationRelativePath = pageName;
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 });
return LocatePageFromPath(pageName, isPartial);
}
else
{
@ -255,105 +247,221 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
private RazorPageResult LocatePageFromViewLocations(
ActionContext context,
private ViewLocationCacheResult LocatePageFromPath(string pageName, bool isPartial)
{
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,
bool isPartial)
{
// Initialize the dictionary for the typical case of having controller and action tokens.
var areaName = GetNormalizedRouteValue(context, AreaKey);
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
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)
{
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
for( var i = 0; i < _viewLocationExpanders.Count; i++)
for (var i = 0; i < _viewLocationExpanders.Count; i++)
{
_viewLocationExpanders[i].PopulateValues(expanderContext);
}
}
// 2. With the values that we've accumumlated so far, check if we have a cached result.
IEnumerable<string> locationsToSearch = null;
var cachedResult = _viewLocationCache.Get(expanderContext);
if (!cachedResult.Equals(ViewLocationCacheResult.None))
{
if (cachedResult.IsFoundResult)
{
var page = _pageFactory.CreateInstance(cachedResult.ViewLocation);
var cacheKey = new ViewLocationCacheKey(
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.ViewName,
expanderContext.IsPartial,
expanderValues);
if (page != null)
{
// 2a We have a cache entry where a view was previously found.
return new RazorPageResult(pageName, page);
}
}
else
{
locationsToSearch = cachedResult.SearchedLocations;
}
ViewLocationCacheResult cacheResult;
if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult))
{
cacheResult = OnCacheMiss(expanderContext, cacheKey);
}
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.
// 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
));
viewLocations = _viewLocationExpanders[i].ExpandViewLocations(expanderContext, viewLocations);
}
// 3. Use the expanded locations to look up a page.
ViewLocationCacheResult cacheResult = null;
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);
if (page != null)
var path = string.Format(
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.
_viewLocationCache.Set(expanderContext, new ViewLocationCacheResult(path, searchedLocations));
return new RazorPageResult(pageName, page);
break;
}
searchedLocations.Add(path);
}
// 3b. We did not find a page for any of the paths.
_viewLocationCache.Set(expanderContext, new ViewLocationCacheResult(searchedLocations));
return new RazorPageResult(pageName, searchedLocations);
// No views were found at the specified location. Create a not found result.
if (cacheResult == null)
{
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(
RazorPageResult result,
IRazorViewFactory razorViewFactory,
ViewLocationCacheResult result,
string viewName,
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);
return ViewEngineResult.Found(result.Name, view);
var page = result.ViewEntry.PageFactory();
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)

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.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Result of <see cref="IViewLocationCache"/> lookups.
/// Result of view location cache lookup.
/// </summary>
public struct ViewLocationCacheResult : IEquatable<ViewLocationCacheResult>
public class ViewLocationCacheResult
{
/// <summary>
/// Initializes a new instance of <see cref="ViewLocationCacheResult"/>
/// for a view that was successfully found at the specified location.
/// </summary>
/// <param name="foundLocation">The view location.</param>
/// <param name="searchedLocations">Locations that were searched
/// in addition to <paramref name="foundLocation"/>.</param>
/// <param name="view">The <see cref="ViewLocationCacheItem"/> for the found view.</param>
/// <param name="viewStarts"><see cref="ViewLocationCacheItem"/>s for applicable _ViewStarts.</param>
public ViewLocationCacheResult(
string foundLocation,
IEnumerable<string> searchedLocations)
: this(searchedLocations)
ViewLocationCacheItem view,
IReadOnlyList<ViewLocationCacheItem> viewStarts)
{
if (foundLocation == null)
if (viewStarts == null)
{
throw new ArgumentNullException(nameof(foundLocation));
throw new ArgumentNullException(nameof(viewStarts));
}
ViewLocation = foundLocation;
SearchedLocations = searchedLocations;
IsFoundResult = true;
ViewEntry = view;
ViewStartEntries = viewStarts;
Success = true;
}
/// <summary>
@ -48,27 +44,26 @@ namespace Microsoft.AspNet.Mvc.Razor
}
SearchedLocations = searchedLocations;
ViewLocation = null;
IsFoundResult = false;
}
/// <summary>
/// A <see cref="ViewLocationCacheResult"/> that represents a cache miss.
/// <see cref="ViewLocationCacheItem"/> for the located view.
/// </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>
/// The location the view was found.
/// <see cref="ViewLocationCacheItem"/>s for applicable _ViewStarts.
/// </summary>
/// <remarks>This is available if <see cref="IsFoundResult"/> is <c>true</c>.</remarks>
public string ViewLocation { get; }
/// <remarks><c>null</c> if <see cref="Success"/> is <c>false</c>.</remarks>
public IReadOnlyList<ViewLocationCacheItem> ViewStartEntries { get; }
/// <summary>
/// The sequence of locations that were searched.
/// </summary>
/// <remarks>
/// When <see cref="IsFoundResult"/> 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
/// When <see cref="Success"/> is <c>true</c> this includes all paths that were search prior to finding
/// a view at <see cref="ViewEntry"/>. When <see cref="Success"/> is <c>false</c>, this includes
/// all search paths.
/// </remarks>
public IEnumerable<string> SearchedLocations { get; }
@ -76,55 +71,6 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Gets a value that indicates whether the view was successfully found.
/// </summary>
public bool IsFoundResult { 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;
}
public bool Success { get; }
}
}

View File

@ -16,10 +16,14 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current executing action.</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>
public ViewLocationExpanderContext(
ActionContext actionContext,
string viewName,
string controllerName,
string areaName,
bool isPartial)
{
if (actionContext == null)
@ -34,6 +38,8 @@ namespace Microsoft.AspNet.Mvc.Razor
ActionContext = actionContext;
ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
IsPartial = isPartial;
}
@ -47,6 +53,16 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </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>
/// Gets a value that determines if a partial view is being discovered.
/// </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
var compilationFailure = new CompilationFailure("test", Enumerable.Empty<Microsoft.Extensions.PlatformAbstractions.DiagnosticMessage>());
var failures = new[] { compilationFailure };
var result = CompilationResult.Failed(failures);
var result = new CompilationResult(failures);
// Act and Assert
Assert.Null(result.CompiledType);

View File

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

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var compiler = new Mock<ICompilationService>();
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());
@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
.Returns(Stream.Null);
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>();
compiler.Setup(c => c.Compile(relativeFileInfo, code))
.Returns(compilationResult)
@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var result = razorService.Compile(relativeFileInfo);
// Assert
Assert.Same(compilationResult, result);
Assert.Same(compilationResult.CompiledType, result.CompiledType);
compiler.Verify();
}

View File

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

View File

@ -1,16 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.OptionsModel;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
@ -116,18 +119,24 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void FindPartialView_ReturnsRazorView_IfLookupWasSuccessful()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
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>()))
.Returns(Mock.Of<IRazorPage>());
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), true))
.Returns(view)
.Verifiable();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]));
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);
// Act
@ -135,9 +144,52 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert
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);
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]
@ -150,8 +202,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var context = GetActionContext(_controllerTestContext);
// Act & Assert
ExceptionAssert.ThrowsArgumentNullOrEmpty(() => viewEngine.FindPartialView(context, partialViewName),
"partialViewName");
ExceptionAssert.ThrowsArgumentNullOrEmpty(
() => viewEngine.FindPartialView(context, partialViewName),
"partialViewName");
}
[Theory]
@ -266,18 +319,24 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void FindView_ReturnsRazorView_IfLookupWasSuccessful()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
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>()))
.Returns(Mock.Of<IRazorPage>());
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(view)
.Verifiable();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]));
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);
// Act
@ -285,63 +344,66 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert
Assert.True(result.Success);
Assert.Same(view, result.View);
var view = Assert.IsType<RazorView>(result.View);
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]
public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr"))
.Returns(page)
.Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewEngine = new OverloadedLocationViewEngine(
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
viewFactory.Object,
GetOptionsAccessor(),
GetViewLocationCache());
GetOptionsAccessor());
viewEngine.SetLocationFormats(
new[] { "fake-path1/{1}/{0}.rzr" },
new[] { "fake-area-path/{2}/{1}/{0}.rzr" });
var context = GetActionContext(_controllerTestContext);
viewFactory.Setup(v => v.GetView(viewEngine, page, false))
.Returns(Mock.Of<IView>());
// Act
var result = viewEngine.FindView(context, "test-view");
// Assert
pageFactory.Verify();
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
}
[Fact]
public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr"))
.Returns(page)
.Setup(p => p.CreateFactory("fake-area-path/foo/bar/test-view2.rzr"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewEngine = new OverloadedLocationViewEngine(
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
viewFactory.Object,
GetOptionsAccessor(),
GetViewLocationCache());
GetOptionsAccessor());
viewEngine.SetLocationFormats(
new[] { "fake-path1/{1}/{0}.rzr" },
new[] { "fake-area-path/{2}/{1}/{0}.rzr" });
var context = GetActionContext(_areaTestContext);
viewFactory.Setup(v => v.GetView(viewEngine, page, false))
.Returns(Mock.Of<IView>());
// Act
var result = viewEngine.FindView(context, "test-view2");
// Assert
pageFactory.Verify();
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
}
[Theory]
@ -351,46 +413,49 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
IEnumerable<string> expectedSeeds)
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml"))
.Returns(Mock.Of<IRazorPage>())
.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 pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("test-string/bar.cshtml"))
.Returns(new RazorPageFactoryResult(() => Mock.Of<IRazorPage>(), new IChangeToken[0]))
.Verifiable();
var expander1Result = new[] { "some-seed" };
var expander1 = new Mock<IViewLocationExpander>();
expander1.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander1.ToString();
})
.Verifiable();
expander1.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
})
.Returns(expander1Result)
.Verifiable();
expander1
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander1.ToString();
})
.Verifiable();
expander1
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
})
.Returns(expander1Result)
.Verifiable();
var expander2 = new Mock<IViewLocationExpander>();
expander2.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.Equal(expander1Result, seeds);
})
.Returns(new[] { "test-string/{1}.cshtml" })
.Verifiable();
expander2
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.Equal(expander1Result, seeds);
})
.Returns(new[] { "test-string/{1}.cshtml" })
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object,
new[] { expander1.Object, expander2.Object });
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander1.Object, expander2.Object });
var context = GetActionContext(routeValues);
// Act
@ -408,130 +473,152 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void FindView_CachesValuesIfViewWasFound()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml"))
.Verifiable();
pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml"))
.Returns(Mock.Of<IRazorPage>())
.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);
})
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(new RazorPageFactoryResult(new IChangeToken[0]))
.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);
// Act
var result = viewEngine.FindView(context, "baz");
// Act 1
var result1 = viewEngine.FindView(context, "baz");
// Assert
Assert.True(result.Success);
// Assert 1
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();
cacheMock.Verify();
}
[Fact]
public void FindView_UsesCachedValueIfViewWasFound()
public void FindView_InvokesPageFactoryIfChangeTokenExpired()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>(MockBehavior.Strict);
pageFactory.Setup(p => p.CreateInstance("some-view-location"))
.Returns(Mock.Of<IRazorPage>())
.Verifiable();
var page1 = Mock.Of<IRazorPage>();
var page2 = Mock.Of<IRazorPage>();
var sequence = new MockSequence();
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>());
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.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);
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 viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "baz");
// Act 1
var result1 = viewEngine.FindView(context, "baz");
// Assert
Assert.True(result.Success);
// Assert 1
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();
cacheMock.Verify();
expander.Verify();
}
[Fact]
public void FindView_LooksForViewsIfCachedViewDoesNotExist()
public void FindView_InvokesPageFactoryIfViewStartExpirationTokensHaveExpired()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("expired-location"))
.Returns((IRazorPage)null)
.Verifiable();
pageFactory.Setup(p => p.CreateInstance("some-view-location"))
.Returns(Mock.Of<IRazorPage>())
.Verifiable();
var page1 = Mock.Of<IRazorPage>();
var page2 = Mock.Of<IRazorPage>();
var viewStart = Mock.Of<IRazorPage>();
var sequence = new MockSequence();
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorViewEngine>(), It.IsAny<IRazorPage>(), false))
.Returns(Mock.Of<IView>());
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.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>();
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 viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "baz");
// Act 1
var result1 = viewEngine.FindView(context, "baz");
// Assert
Assert.True(result.Success);
// Assert 1
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();
cacheMock.Verify();
expander.Verify();
}
// 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.
[Fact]
public void FindView_DoesNotInvokeExpandViewLocations_IfCacheEntryMatchesButViewIsNotFound()
public void FindView_DoesNotInvokeViewLocationExpanders_IfChangeTokenHasNotExpired()
{
// Arrange
var pageFactory = Mock.Of<IRazorPageFactory>();
var viewFactory = Mock.Of<IRazorViewFactory>();
var cache = new DefaultViewLocationCache();
var pageFactory = Mock.Of<IRazorPageFactoryProvider>();
var expander = new Mock<IViewLocationExpander>();
var expandedLocations = new[]
{
@ -548,16 +635,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable();
expander
.Setup(v => v.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()))
.Returns((ViewLocationExpanderContext c, IEnumerable<string> viewLocations) => expandedLocations)
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(expandedLocations)
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory,
viewFactory,
expanders: new[] { expander.Object },
cache: cache);
expanders: new[] { expander.Object });
var context = GetActionContext(_controllerTestContext);
// Act - 1
@ -582,6 +667,70 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
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]
[InlineData(null)]
[InlineData("")]
@ -604,36 +753,39 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("expanded-path/bar-layout"))
.Returns(page)
.Verifiable();
var viewFactory = new Mock<IRazorViewFactory>(MockBehavior.Strict);
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("expanded-path/bar-layout"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var expander = new Mock<IViewLocationExpander>();
expander.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander.ToString();
})
.Verifiable();
expander.Setup(e => e.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander.ToString();
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.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}" };
})
.Verifiable();
return new[] { "expanded-path/bar-{0}" };
})
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object,
new[] { expander.Object });
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander.Object });
var context = GetActionContext(routeValues);
// Act
@ -686,10 +838,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
};
var page = new Mock<IRazorPage>(MockBehavior.Strict).Object;
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("/Views/Foo/details.cshtml"))
.Returns(page)
.Verifiable();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("/Views/Foo/details.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var routesInActionDescriptor = new Dictionary<string, string>()
@ -1092,23 +1245,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}
}
private RazorViewEngine CreateViewEngine(
IRazorPageFactory pageFactory = null,
IRazorViewFactory viewFactory = null,
IEnumerable<IViewLocationExpander> expanders = null,
IViewLocationCache cache = null)
private TestableRazorViewEngine CreateViewEngine(
IRazorPageFactoryProvider pageFactory = null,
IEnumerable<IViewLocationExpander> expanders = null)
{
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>();
viewFactory = viewFactory ?? Mock.Of<IRazorViewFactory>();
cache = cache ?? GetViewLocationCache();
var viewEngine = new RazorViewEngine(pageFactory,
viewFactory,
GetOptionsAccessor(expanders),
cache);
return viewEngine;
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactoryProvider>();
return new TestableRazorViewEngine(
pageFactory,
GetOptionsAccessor(expanders));
}
private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
@ -1129,15 +1273,6 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
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)
{
var httpContext = new DefaultHttpContext();
@ -1185,32 +1320,33 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
return new ActionContext(httpContext, routeData, actionDescriptor);
}
private class OverloadedLocationViewEngine : RazorViewEngine
private class TestableRazorViewEngine : RazorViewEngine
{
public OverloadedLocationViewEngine(
IRazorPageFactory pageFactory,
IRazorViewFactory viewFactory,
IOptions<RazorViewEngineOptions> optionsAccessor,
IViewLocationCache cache)
: base(pageFactory, viewFactory, optionsAccessor, cache)
private IEnumerable<string> _viewLocationFormats;
private IEnumerable<string> _areaViewLocationFormats;
public TestableRazorViewEngine(
IRazorPageFactoryProvider pageFactory,
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
{
return new[] { "fake-path1/{1}/{0}.rzr" };
}
_viewLocationFormats = viewLocationFormats;
_areaViewLocationFormats = areaViewLocationFormats;
}
public override IEnumerable<string> AreaViewLocationFormats
{
get
{
return new[] { "fake-area-path/{2}/{1}/{0}.rzr" };
}
}
public override IEnumerable<string> ViewLocationFormats =>
_viewLocationFormats != null ? _viewLocationFormats : base.ViewLocationFormats;
public override IEnumerable<string> AreaViewLocationFormats =>
_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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal;
@ -14,6 +13,7 @@ using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.AspNet.Routing;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
@ -39,12 +39,13 @@ namespace Microsoft.AspNet.Mvc.Razor
v.HtmlEncoder = new HtmlTestEncoder();
v.Write("Hello world");
});
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
new HtmlTestEncoder(),
isPartial: true);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view);
var expected = viewContext.Writer;
@ -68,12 +69,13 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Same(viewData, v.ViewContext.ViewData);
});
var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object,
CreateViewStartProvider(),
page,
new HtmlTestEncoder(),
isPartial: true);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
activator.Object,
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view);
var expectedWriter = viewContext.Writer;
@ -132,12 +134,13 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(new RazorPageResult(LayoutPath, layout));
var view = new RazorView(viewEngine.Object,
activator,
CreateViewStartProvider(viewStart),
page,
new HtmlTestEncoder(),
isPartial: false);
var view = new RazorView(
viewEngine.Object,
activator,
new[] { viewStart },
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
var expectedWriter = viewContext.Writer;
@ -157,12 +160,13 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object,
CreateViewStartProvider(),
page,
new HtmlTestEncoder(),
isPartial: true);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
activator.Object,
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view);
// Act
@ -193,29 +197,28 @@ namespace Microsoft.AspNet.Mvc.Razor
v.Write("layout-content" + Environment.NewLine);
v.RenderBodyPublic();
});
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance(LayoutPath))
.Returns(layout);
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory(LayoutPath))
.Returns(new RazorPageFactoryResult(() => layout, new IChangeToken[0]));
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath))
.Returns(new RazorPageResult(LayoutPath, layout));
var viewStartProvider = CreateViewStartProvider();
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
viewStartProvider,
page,
new HtmlTestEncoder(),
isPartial: true);
var view = new RazorView(
viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: true);
var viewContext = CreateViewContext(view);
// Act
await view.RenderAsync(viewContext);
// Assert
Mock.Get(viewStartProvider)
.Verify(v => v.GetViewStartPages(It.IsAny<string>()), Times.Never());
Assert.Equal(expected, viewContext.Writer.ToString());
}
@ -228,12 +231,13 @@ namespace Microsoft.AspNet.Mvc.Razor
{
actual = v.Output;
});
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
new HtmlTestEncoder(),
isPartial: false);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
var original = viewContext.Writer;
@ -253,12 +257,13 @@ namespace Microsoft.AspNet.Mvc.Razor
{
v.WriteLiteral("Hello world");
});
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page,
new HtmlTestEncoder(),
isPartial: false);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
var original = viewContext.Writer;
@ -280,12 +285,13 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object,
CreateViewStartProvider(),
page,
new HtmlTestEncoder(),
isPartial: false);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
activator.Object,
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
// Act
@ -323,12 +329,13 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable();
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
.Verifiable();
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
activator.Object,
CreateViewStartProvider(viewStart1, viewStart2),
page,
new HtmlTestEncoder(),
isPartial: false);
var view = new RazorView(
Mock.Of<IRazorViewEngine>(),
activator.Object,
new[] { viewStart1, viewStart2 },
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
// Act
@ -358,7 +365,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var activator = new Mock<IRazorPageActivator>();
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -422,7 +429,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
activator.Object,
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -464,7 +471,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -527,7 +534,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -587,7 +594,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -647,7 +654,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -712,7 +719,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -744,7 +751,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -808,7 +815,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -844,7 +851,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -889,7 +896,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -956,7 +963,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1009,7 +1016,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1059,7 +1066,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1089,7 +1096,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1134,7 +1141,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1204,13 +1211,9 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "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,
Mock.Of<IRazorPageActivator>(),
viewStartProvider.Object,
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1222,7 +1225,6 @@ namespace Microsoft.AspNet.Mvc.Razor
// Assert
feature.Verify();
viewStartProvider.Verify();
Assert.True(layoutExecuted);
}
@ -1256,7 +1258,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: true);
@ -1288,7 +1290,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial);
@ -1328,7 +1330,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(viewEngine,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart1, viewStart2),
new[] { viewStart1, viewStart2 },
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1364,12 +1366,13 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var viewEngine = Mock.Of<IRazorViewEngine>();
var view = new RazorView(viewEngine,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart1, viewStart2),
page,
new HtmlTestEncoder(),
isPartial: false);
var view = new RazorView(
viewEngine,
Mock.Of<IRazorPageActivator>(),
new[] { viewStart1, viewStart2 },
page,
new HtmlTestEncoder(),
isPartial: false);
var viewContext = CreateViewContext(view);
// Act
@ -1402,13 +1405,14 @@ namespace Microsoft.AspNet.Mvc.Razor
v.RenderBodyPublic();
});
var viewEngine = new Mock<IRazorViewEngine>();
viewEngine.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "/Layout.cshtml"))
.Returns(new RazorPageResult("Layout", layout));
viewEngine
.Setup(p => p.FindPage(It.IsAny<ActionContext>(), "/Layout.cshtml"))
.Returns(new RazorPageResult("Layout", layout));
var view = new RazorView(
viewEngine.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(viewStart),
new[] { viewStart },
page,
new HtmlTestEncoder(),
isPartial: false);
@ -1434,7 +1438,7 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var view = new RazorView(Mock.Of<IRazorViewEngine>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
new IRazorPage[0],
page,
new HtmlTestEncoder(),
isPartial: true);
@ -1447,6 +1451,48 @@ namespace Microsoft.AspNet.Mvc.Razor
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()
{
var mockWriter = new Mock<TextWriter>();
@ -1469,17 +1515,6 @@ namespace Microsoft.AspNet.Mvc.Razor
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 readonly Action<TestableRazorPage> _executeAction;