diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs index d1582323e7..16318d9b80 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs @@ -10,83 +10,63 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation /// /// Represents the result of compilation. /// - public class CompilationResult + public struct CompilationResult { /// - /// Creates a new instance of . - /// - protected CompilationResult() - { - } - - /// - /// Gets (or sets in derived types) the type produced as a result of compilation. - /// - /// This property is null when compilation failed. - public Type CompiledType { get; protected set; } - - /// - /// Gets (or sets in derived types) the generated C# content that was compiled. - /// - public string CompiledContent { get; protected set; } - - /// - /// Gets the s produced from parsing or compiling the Razor file. - /// - /// This property is null when compilation succeeded. An empty sequence - /// indicates a failed compilation. - public IEnumerable CompilationFailures { get; private set; } - - /// - /// Gets the . - /// - /// The current instance. - /// Thrown if compilation failed. - public CompilationResult EnsureSuccessful() - { - if (CompilationFailures != null) - { - throw new CompilationFailedException(CompilationFailures); - } - - return this; - } - - /// - /// Creates a for a failed compilation. - /// - /// s produced from parsing or - /// compiling the Razor file. - /// A instance for a failed compilation. - public static CompilationResult Failed(IEnumerable compilationFailures) - { - if (compilationFailures == null) - { - throw new ArgumentNullException(nameof(compilationFailures)); - } - - return new CompilationResult - { - CompilationFailures = compilationFailures - }; - } - - /// - /// Creates a for a successful compilation. + /// Initializes a new instance of for a successful compilation. /// /// The compiled type. - /// A instance for a successful compilation. - 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; + } + + /// + /// Initializes a new instance of for a failed compilation. + /// + /// s produced from parsing or + /// compiling the Razor file. + public CompilationResult(IEnumerable compilationFailures) + { + if (compilationFailures == null) { - CompiledType = type - }; + throw new ArgumentNullException(nameof(compilationFailures)); + } + + CompiledType = null; + CompilationFailures = compilationFailures; + } + + /// + /// Gets the type produced as a result of compilation. + /// + /// This property is null when compilation failed. + public Type CompiledType { get; } + + /// + /// Gets the s produced from parsing or compiling the Razor file. + /// + /// This property is null when compilation succeeded. An empty sequence + /// indicates a failed compilation. + public IEnumerable CompilationFailures { get; } + + /// + /// Gets the . + /// + /// The current instance. + /// Thrown if compilation failed. + public void EnsureSuccessful() + { + if (CompilationFailures != null) + { + throw new CompilationFailedException(CompilationFailures); + } } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 0a44ad1b26..da6bb51cf0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -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 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; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs index 130ab74c15..2425e33d73 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs @@ -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 { /// /// Result of . /// - public class CompilerCacheResult + public struct CompilerCacheResult { - /// - /// Result of when the specified file does not exist in the - /// file system. - /// - public static CompilerCacheResult FileNotFound { get; } = new CompilerCacheResult(); - /// /// Initializes a new instance of with the specified - /// . + /// . /// - /// The + /// The . public CompilerCacheResult(CompilationResult compilationResult) + : this(compilationResult, new IChangeToken[0]) { - if (compilationResult == null) - { - throw new ArgumentNullException(nameof(compilationResult)); - } - - CompilationResult = compilationResult; } /// - /// Initializes a new instance of for a failed file lookup. + /// Initializes a new instance of with the specified + /// . /// - protected CompilerCacheResult() + /// The . + /// One or more instances that indicate when + /// this result has expired. + public CompilerCacheResult(CompilationResult compilationResult, IList expirationTokens) { + if (expirationTokens == null) + { + throw new ArgumentNullException(nameof(expirationTokens)); + } + + CompilationResult = compilationResult; + Success = true; + ExpirationTokens = expirationTokens; + } + + /// + /// Initializes a new instance of for a file that could not be + /// found in the file system. + /// + /// One or more instances that indicate when + /// this result has expired. + public CompilerCacheResult(IList expirationTokens) + { + if (expirationTokens == null) + { + throw new ArgumentNullException(nameof(expirationTokens)); + } + + CompilationResult = default(CompilationResult); + Success = false; + ExpirationTokens = expirationTokens; } /// /// The . /// - /// This property is null when file lookup failed. + /// This property is not available when is false. public CompilationResult CompilationResult { get; } + + /// + /// instances that indicate when this result has expired. + /// + public IList ExpirationTokens { get; } + + /// + /// Gets a value that determines if the view was successfully found and compiled. + /// + public bool Success { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs index 2b7e89cf83..92cadaa155 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs @@ -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) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs index 8f3abe62bb..09b5ae634d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs @@ -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) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs deleted file mode 100644 index 0e96345135..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs +++ /dev/null @@ -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 -{ - /// - /// Represents the result of compilation that does not come from the . - /// - public class UncachedCompilationResult : CompilationResult - { - private UncachedCompilationResult() - { - } - - public string RazorFileContent { get; private set; } - - /// - /// Creates a that represents a success in compilation. - /// - /// The compiled type. - /// The generated C# content that was compiled. - /// An instance that indicates a successful - /// compilation. - 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, - }; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/DefaultRazorPageFactoryProvider.cs similarity index 58% rename from src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs rename to src/Microsoft.AspNet.Mvc.Razor/DefaultRazorPageFactoryProvider.cs index 93f1d60b22..5d92b0074c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/DefaultRazorPageFactoryProvider.cs @@ -7,10 +7,10 @@ using Microsoft.AspNet.Mvc.Razor.Compilation; namespace Microsoft.AspNet.Mvc.Razor { /// - /// Represents a that creates instances + /// Represents a that creates instances /// from razor files in the file system. /// - public class VirtualPathRazorPageFactory : IRazorPageFactory + public class DefaultRazorPageFactoryProvider : IRazorPageFactoryProvider { /// /// This delegate holds on to an instance of . @@ -20,11 +20,11 @@ namespace Microsoft.AspNet.Mvc.Razor private ICompilerCache _compilerCache; /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// /// The . /// The . - public VirtualPathRazorPageFactory( + public DefaultRazorPageFactoryProvider( IRazorCompilationService razorCompilationService, ICompilerCacheProvider compilerCacheProvider) { @@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - 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; + /// + /// Creates a factory for . + /// + /// The to produce an instance of + /// from. + /// The application relative path of the page. + /// A factory for . + protected virtual Func GetPageFactory(Type compiledType, string relativePath) + { + return () => + { + var page = (IRazorPage)Activator.CreateInstance(compiledType); + page.Path = relativePath; + return page; + }; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs b/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs deleted file mode 100644 index 1530e299c4..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs +++ /dev/null @@ -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 -{ - /// - /// Default implementation of . - /// - public class DefaultViewLocationCache : IViewLocationCache - { - // A mapping of keys generated from ViewLocationExpanderContext to view locations. - private readonly ConcurrentDictionary _cache; - - /// - /// Initializes a new instance of . - /// - public DefaultViewLocationCache() - { - _cache = new ConcurrentDictionary( - ViewLocationCacheKeyComparer.Instance); - } - - /// - 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; - } - - /// - 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(values, StringComparer.Ordinal); - } - - return new ViewLocationCacheKey( - context.ViewName, - controller, - area, - context.IsPartial, - values); - } - - // Internal for unit testing - internal class ViewLocationCacheKeyComparer : IEqualityComparer - { - 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 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 Values { get; } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 23625126bd..15d26bfba0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -131,8 +131,6 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); - // Caches view locations that are valid for the lifetime of the application. - services.TryAddSingleton(); services.TryAdd(ServiceDescriptor.Singleton(serviceProvider => { var cachedFileProvider = serviceProvider.GetRequiredService>(); @@ -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(); - services.TryAddTransient(); + services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); services.TryAddTransient(); // This caches Razor page activation details that are valid for the lifetime of the application. diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactoryProvider.cs similarity index 63% rename from src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs rename to src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactoryProvider.cs index b4cc1a011e..7f183c853d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactoryProvider.cs @@ -6,13 +6,13 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Defines methods that are used for creating instances at a given path. /// - public interface IRazorPageFactory + public interface IRazorPageFactoryProvider { /// - /// Creates a for the specified path. + /// Creates a factory for the specified path. /// /// The path to locate the page. - /// The IRazorPage instance if it exists, null otherwise. - IRazorPage CreateInstance(string relativePath); + /// The instance. + RazorPageFactoryResult CreateFactory(string relativePath); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs deleted file mode 100644 index c49731f8ae..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs +++ /dev/null @@ -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 -{ - /// - /// Defines methods to create instances with a given . - /// - public interface IRazorViewFactory - { - /// - /// Creates a providing it with the to execute. - /// - /// The that was used to locate Layout pages - /// that will be part of 's execution. - /// The instance to execute. - /// Determines if the view is to be executed as a partial. - /// A instance that renders the contents of the - IView GetView(IRazorViewEngine viewEngine, IRazorPage page, bool isPartial); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewLocationCache.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewLocationCache.cs deleted file mode 100644 index e3378a8890..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/IViewLocationCache.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Mvc.Razor -{ - /// - /// Specifies the contracts for caching view locations generated by . - /// - public interface IViewLocationCache - { - /// - /// Gets a cached view location based on the specified . - /// - /// The for the current view location - /// expansion. - /// The cached location, if available, null otherwise. - ViewLocationCacheResult Get(ViewLocationExpanderContext context); - - /// - /// Adds a cache entry for values specified by . - /// - /// The for the current view location - /// expansion. - /// The view location that is to be cached. - void Set(ViewLocationExpanderContext context, ViewLocationCacheResult value); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs deleted file mode 100644 index f5f711dce7..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/IViewStartProvider.cs +++ /dev/null @@ -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 -{ - /// - /// Defines methods for locating ViewStart pages that are applicable to a page. - /// - public interface IViewStartProvider - { - /// - /// Given a view path, returns a sequence of ViewStart instances - /// that are applicable to the specified view. - /// - /// The path of the page to locate ViewStart files for. - /// A sequence of that represent ViewStart. - IEnumerable GetViewStartPages(string path); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs index db06d8a689..64d78d3443 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorPreCompiler.cs @@ -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); } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageFactoryResult.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageFactoryResult.cs new file mode 100644 index 0000000000..eed74e32b1 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageFactoryResult.cs @@ -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 +{ + /// + /// Result of . + /// + public struct RazorPageFactoryResult + { + /// + /// Initializes a new instance of with the + /// specified . + /// + /// One or more instances. + public RazorPageFactoryResult(IList expirationTokens) + { + if (expirationTokens == null) + { + throw new ArgumentNullException(nameof(expirationTokens)); + } + + ExpirationTokens = expirationTokens; + RazorPageFactory = null; + } + + /// + /// Initializes a new instance of with the + /// specified factory. + /// + /// The factory. + /// One or more instances. + public RazorPageFactoryResult( + Func razorPageFactory, + IList expirationTokens) + { + if (razorPageFactory == null) + { + throw new ArgumentNullException(nameof(razorPageFactory)); + } + + if (expirationTokens == null) + { + throw new ArgumentNullException(nameof(expirationTokens)); + } + + RazorPageFactory = razorPageFactory; + ExpirationTokens = expirationTokens; + } + + /// + /// The factory. + /// + /// This property is null when is false. + public Func RazorPageFactory { get; } + + /// + /// One or more s associated with this instance of + /// . + /// + public IList ExpirationTokens { get; } + + /// + /// Gets a value that determines if the page was successfully located. + /// + public bool Success => RazorPageFactory != null; + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs index 92628d18a5..89ee7a66e6 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; namespace Microsoft.AspNet.Mvc.Razor { /// - /// Represents the results of locating a . + /// Result of locating a . /// - public class RazorPageResult + public struct RazorPageResult { /// /// Initializes a new instance of for a successful discovery. @@ -30,6 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor Name = name; Page = page; + SearchedLocations = null; } /// @@ -50,6 +51,7 @@ namespace Microsoft.AspNet.Mvc.Razor } Name = name; + Page = null; SearchedLocations = searchedLocations; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 5c5620203f..05b58c63ca 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -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 /// /// The used to locate Layout pages. /// The used to activate pages. - /// The used for discovery of _ViewStart + /// The sequence of instances executed as _ViewStarts. + /// /// The instance to execute. /// The HTML encoder. /// Determines if the view is to be executed as a partial. - /// pages public RazorView( IRazorViewEngine viewEngine, IRazorPageActivator pageActivator, - IViewStartProvider viewStartProvider, + IReadOnlyList 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 /// public bool IsPartial { get; } + /// + /// Gets the sequence of _ViewStart instances + /// that are executed by this view if is false. + /// + public IReadOnlyList 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; diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 50bf30c650..e2cf7fc24d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -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 _viewLocationFormats = new[] - { - "/Views/{1}/{0}" + ViewExtension, - "/Views/Shared/{0}" + ViewExtension, - }; - - private static readonly IEnumerable _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 _viewLocationExpanders; - private readonly IViewLocationCache _viewLocationCache; + private readonly IRazorPageActivator _pageActivator; + private readonly HtmlEncoder _htmlEncoder; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the . /// - /// The page factory used for creating instances. public RazorViewEngine( - IRazorPageFactory pageFactory, - IRazorViewFactory viewFactory, - IOptions optionsAccessor, - IViewLocationCache viewLocationCache) + IRazorPageFactoryProvider pageFactory, + IRazorPageActivator pageActivator, + HtmlEncoder htmlEncoder, + IOptions optionsAccessor) { _pageFactory = pageFactory; - _viewFactory = viewFactory; + _pageActivator = pageActivator; _viewLocationExpanders = optionsAccessor.Value.ViewLocationExpanders; - _viewLocationCache = viewLocationCache; + _htmlEncoder = htmlEncoder; + ViewLookupCache = new MemoryCache(new MemoryCacheOptions + { + CompactOnMemoryPressure = false + }); } /// @@ -72,10 +67,11 @@ namespace Microsoft.AspNet.Mvc.Razor /// For example, the view for the Test action of HomeController should be located at /// /Views/Home/Test.cshtml. Locations such as /views/home/test.cshtml would not be discovered /// - public virtual IEnumerable ViewLocationFormats + public virtual IEnumerable ViewLocationFormats { get; } = new[] { - get { return _viewLocationFormats; } - } + "/Views/{1}/{0}" + ViewExtension, + "/Views/Shared/{0}" + ViewExtension, + }; /// /// Gets the locations where this instance of will search for views within an @@ -92,15 +88,20 @@ namespace Microsoft.AspNet.Mvc.Razor /// For example, the view for the Test action of HomeController should be located at /// /Views/Home/Test.cshtml. Locations such as /views/home/test.cshtml would not be discovered /// - public virtual IEnumerable AreaViewLocationFormats + public virtual IEnumerable AreaViewLocationFormats { get; } = new[] { - get { return _areaViewLocationFormats; } - } + "/Areas/{2}/Views/{1}/{0}" + ViewExtension, + "/Areas/{2}/Views/Shared/{0}" + ViewExtension, + "/Views/Shared/{0}" + ViewExtension, + }; + + /// + /// A cache for results of view lookups. + /// + protected IMemoryCache ViewLookupCache { get; } /// - 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); } /// - 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); } /// - 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); + } } /// @@ -166,9 +172,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// for traditional routes to get route values /// produces consistently cased results. /// - 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(); + 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( + 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 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(StringComparer.Ordinal); + expanderValues = new Dictionary(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 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(); - foreach (var path in locationsToSearch) + var expirationTokens = new HashSet(); + 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(cacheKey, cacheResult, cacheEntryOptions); + } + + private ViewLocationCacheResult CreateCacheResult( + ViewLocationCacheKey cacheKey, + HashSet 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 GetViewStartPages( + string path, + HashSet expirationTokens) + { + var viewStartPages = new List(); + 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) diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs deleted file mode 100644 index 661926a481..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs +++ /dev/null @@ -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 -{ - /// - /// Represents the default implementation that creates - /// instances with a given . - /// - public class RazorViewFactory : IRazorViewFactory - { - private readonly HtmlEncoder _htmlEncoder; - private readonly IRazorPageActivator _pageActivator; - private readonly IViewStartProvider _viewStartProvider; - - /// - /// Initializes a new instance of RazorViewFactory - /// - /// The used to activate pages. - /// The used for discovery of _ViewStart - /// pages - public RazorViewFactory( - IRazorPageActivator pageActivator, - IViewStartProvider viewStartProvider, - HtmlEncoder htmlEncoder) - { - _pageActivator = pageActivator; - _viewStartProvider = viewStartProvider; - _htmlEncoder = htmlEncoder; - } - - /// - 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; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheItem.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheItem.cs new file mode 100644 index 0000000000..a87488c423 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheItem.cs @@ -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 +{ + /// + /// An item in . + /// + public struct ViewLocationCacheItem + { + /// + /// Initializes a new instance of . + /// + /// The factory. + /// The application relative path of the . + public ViewLocationCacheItem(Func razorPageFactory, string location) + { + PageFactory = razorPageFactory; + Location = location; + } + + /// + /// Gets the application relative path of the + /// + public string Location { get; } + + /// + /// Gets the factory. + /// + public Func PageFactory { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheKey.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheKey.cs new file mode 100644 index 0000000000..f73900402d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheKey.cs @@ -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 +{ + /// + /// Key for entries in . + /// + public struct ViewLocationCacheKey : IEquatable + { + /// + /// Initializes a new instance of . + /// + /// The view name or path. + /// Determines if the view is a partial. + public ViewLocationCacheKey( + string viewName, + bool isPartial) + : this( + viewName, + controllerName: null, + areaName: null, + isPartial: isPartial, + values: null) + { + } + + /// + /// Initializes a new instance of . + /// + /// The view name. + /// The controller name. + /// The area name. + /// Determines if the view is a partial. + /// Values from instances. + public ViewLocationCacheKey( + string viewName, + string controllerName, + string areaName, + bool isPartial, + IReadOnlyDictionary values) + { + ViewName = viewName; + ControllerName = controllerName; + AreaName = areaName; + IsPartial = isPartial; + ViewLocationExpanderValues = values; + } + + /// + /// Gets the view name. + /// + public string ViewName { get; } + + /// + /// Gets the controller name. + /// + public string ControllerName { get; } + + /// + /// Gets the area name. + /// + public string AreaName { get; } + + /// + /// Determines if the view is a partial. + /// + public bool IsPartial { get; } + + /// + /// Gets the values populated by instances. + /// + public IReadOnlyDictionary ViewLocationExpanderValues { get; } + + /// + 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; + } + + /// + public override bool Equals(object obj) + { + if (obj is ViewLocationCacheKey) + { + return Equals((ViewLocationCacheKey)obj); + } + + return false; + } + + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheResult.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheResult.cs index 3f27f16636..80ff746c5e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheResult.cs @@ -3,36 +3,32 @@ using System; using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNet.Mvc.Razor { /// - /// Result of lookups. + /// Result of view location cache lookup. /// - public struct ViewLocationCacheResult : IEquatable + public class ViewLocationCacheResult { /// /// Initializes a new instance of /// for a view that was successfully found at the specified location. /// - /// The view location. - /// Locations that were searched - /// in addition to . + /// The for the found view. + /// s for applicable _ViewStarts. public ViewLocationCacheResult( - string foundLocation, - IEnumerable searchedLocations) - : this(searchedLocations) + ViewLocationCacheItem view, + IReadOnlyList 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; } /// @@ -48,27 +44,26 @@ namespace Microsoft.AspNet.Mvc.Razor } SearchedLocations = searchedLocations; - ViewLocation = null; - IsFoundResult = false; } /// - /// A that represents a cache miss. + /// for the located view. /// - public static readonly ViewLocationCacheResult None = new ViewLocationCacheResult(Enumerable.Empty()); + /// null if is false. + public ViewLocationCacheItem ViewEntry { get; } /// - /// The location the view was found. + /// s for applicable _ViewStarts. /// - /// This is available if is true. - public string ViewLocation { get; } + /// null if is false. + public IReadOnlyList ViewStartEntries { get; } /// /// The sequence of locations that were searched. /// /// - /// When is true this includes all paths that were search prior to finding - /// a view at . When is false, this includes + /// When is true this includes all paths that were search prior to finding + /// a view at . When is false, this includes /// all search paths. /// public IEnumerable SearchedLocations { get; } @@ -76,55 +71,6 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Gets a value that indicates whether the view was successfully found. /// - public bool IsFoundResult { get; } - - /// - 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); - } - } - - /// - 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; } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs index 35c4f6bc04..f120ccb7ee 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs @@ -16,10 +16,14 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// The for the current executing action. /// The view name. + /// The controller name. + /// The area name. /// Determines if the view being discovered is a partial. 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 /// public string ViewName { get; } + /// + /// Gets the controller name. + /// + public string ControllerName { get; } + + /// + /// Gets the area name. + /// + public string AreaName { get; } + /// /// Gets a value that determines if a partial view is being discovered. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs deleted file mode 100644 index c0bdcde162..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewStartProvider.cs +++ /dev/null @@ -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 -{ - /// - public class ViewStartProvider : IViewStartProvider - { - private readonly IRazorPageFactory _pageFactory; - - public ViewStartProvider(IRazorPageFactory pageFactory) - { - _pageFactory = pageFactory; - } - - /// - public IEnumerable 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; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs index c87305defd..d2f78df1b5 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation // Arrange var compilationFailure = new CompilationFailure("test", Enumerable.Empty()); var failures = new[] { compilationFailure }; - var result = CompilationResult.Failed(failures); + var result = new CompilationResult(failures); // Act and Assert Assert.Null(result.CompiledType); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index 1b11ba0010..3203c8920c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -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(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(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(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); } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs index e7717aebbd..5d0079f9a2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation var compiler = new Mock(); compiler.Setup(c => c.Compile(relativeFileInfo, It.IsAny())) - .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(); 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(); } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs index 4666a09127..64c10a3e06 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs @@ -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(); - 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(); 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(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(); - 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(); 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(); - 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(); - 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(); - 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(); var mockFileInfo = new Mock(); @@ -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(); - 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(); 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(); - 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(); 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(); - 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(); - libraryExporter.Setup(l => l.GetAllExports(It.IsAny())) - .Returns(libraryExport); + libraryExporter + .Setup(l => l.GetAllExports(It.IsAny())) + .Returns(libraryExport); return libraryExporter.Object; } private IApplicationEnvironment GetApplicationEnvironment() { var applicationEnvironment = new Mock(); - 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>(); - options.SetupGet(o => o.Value) + options + .SetupGet(o => o.Value) .Returns(razorViewEngineOptions); return options.Object; diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultRazorPageFactoryProviderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultRazorPageFactoryProviderTest.cs new file mode 100644 index 0000000000..dcd6d59cc5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultRazorPageFactoryProviderTest.cs @@ -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(), + Mock.Of(), + }; + var compilerCache = new Mock(); + compilerCache + .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) + .Returns(new CompilerCacheResult(expirationTokens)); + var compilerCacheProvider = new Mock(); + compilerCacheProvider + .SetupGet(c => c.Cache) + .Returns(compilerCache.Object); + var factoryProvider = new DefaultRazorPageFactoryProvider( + Mock.Of(), + 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(), + Mock.Of(), + }; + var compilerCache = new Mock(); + compilerCache + .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) + .Returns(new CompilerCacheResult(new CompilationResult(typeof(object)), expirationTokens)); + var compilerCacheProvider = new Mock(); + compilerCacheProvider + .SetupGet(c => c.Cache) + .Returns(compilerCache.Object); + var factoryProvider = new DefaultRazorPageFactoryProvider( + Mock.Of(), + 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(); + compilerCache + .Setup(f => f.GetOrAdd(It.IsAny(), It.IsAny>())) + .Returns(new CompilerCacheResult(new CompilationResult(typeof(TestRazorPage)), new IChangeToken[0])); + var compilerCacheProvider = new Mock(); + compilerCacheProvider + .SetupGet(c => c.Cache) + .Returns(compilerCache.Object); + var factoryProvider = new DefaultRazorPageFactoryProvider( + Mock.Of(), + 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(); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs deleted file mode 100644 index 1561f262bc..0000000000 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs +++ /dev/null @@ -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 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(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 - { - { "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 - { - { "somekey", "somevalue" } - }; - - var viewLocationExpanderContext2 = new ViewLocationExpanderContext( - GetActionContext("Controller1", "Area1"), - "View1", - isPartial: false); - viewLocationExpanderContext2.Values = new Dictionary - { - { "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 - { - { keyName1, "somevalue" } - }; - - var viewLocationExpanderContext2 = new ViewLocationExpanderContext( - GetActionContext("Controller1", "Area1"), - "View1", - isPartial: false); - viewLocationExpanderContext2.Values = new Dictionary - { - { 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 - { - { "somekey", value1 } - }; - - var viewLocationExpanderContext2 = new ViewLocationExpanderContext( - GetActionContext("Controller1", "Area1"), - "View1", - isPartial: false); - viewLocationExpanderContext2.Values = new Dictionary - { - { "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 - { - { "somekey1", "value1" }, - { "somekey2", "value2" }, - }; - - var viewLocationExpanderContext2 = new ViewLocationExpanderContext( - GetActionContext("Controller1", "Area1"), - "View1", - isPartial: false); - viewLocationExpanderContext2.Values = new Dictionary - { - { "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(); - return new ActionContext(new DefaultHttpContext(), routeData, actionDesciptor); - } - - private static ActionContext GetActionContextWithActionDescriptor( - IDictionary routeValues, - IDictionary 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(); - foreach (var kvp in routesInActionDescriptor) - { - actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value)); - } - } - - return new ActionContext(httpContext, routeData, actionDescriptor); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs index 3b039c94d5..7bc9744567 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs @@ -131,7 +131,12 @@ namespace Microsoft.AspNet.Mvc.Razor IEnumerable 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(); viewLocationExpanderContext.Values["language"] = "en-GB"; @@ -150,7 +155,12 @@ namespace Microsoft.AspNet.Mvc.Razor public void ExpandViewLocations_NullContextValue(IEnumerable 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(); @@ -168,7 +178,12 @@ namespace Microsoft.AspNet.Mvc.Razor public void ExpandViewLocations_IncorrectLocaleContextValue(IEnumerable 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(); viewLocationExpanderContext.Values["language"] = "!-invalid-culture-!"; diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 9dc7b72795..81265e473a 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -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(); - var viewFactory = new Mock(); + var pageFactory = new Mock(); var page = Mock.Of(); - var view = Mock.Of(); + var viewStart1 = Mock.Of(); + var viewStart2 = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance(It.IsAny())) - .Returns(Mock.Of()); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), 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(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(); + var page = Mock.Of(); + var viewStart = Mock.Of(); + 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(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(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(); - var viewFactory = new Mock(); + var pageFactory = new Mock(); var page = Mock.Of(); - var view = Mock.Of(); + var viewStart1 = Mock.Of(); + var viewStart2 = Mock.Of(); - pageFactory.Setup(p => p.CreateInstance(It.IsAny())) - .Returns(Mock.Of()); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), 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(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(); - var viewFactory = new Mock(); + var pageFactory = new Mock(); + var page = Mock.Of(); 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()); // Act var result = viewEngine.FindView(context, "test-view"); // Assert pageFactory.Verify(); + var view = Assert.IsType(result.View); + Assert.Same(page, view.RazorPage); } [Fact] public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea() { // Arrange - var pageFactory = new Mock(); - var viewFactory = new Mock(); + var pageFactory = new Mock(); var page = Mock.Of(); 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()); // Act var result = viewEngine.FindView(context, "test-view2"); // Assert pageFactory.Verify(); + var view = Assert.IsType(result.View); + Assert.Same(page, view.RazorPage); } [Theory] @@ -351,46 +413,49 @@ namespace Microsoft.AspNet.Mvc.Razor.Test IEnumerable expectedSeeds) { // Arrange - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml")) - .Returns(Mock.Of()) - .Verifiable(); - - var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Mock.Of()); + var pageFactory = new Mock(); + pageFactory + .Setup(p => p.CreateFactory("test-string/bar.cshtml")) + .Returns(new RazorPageFactoryResult(() => Mock.Of(), new IChangeToken[0])) + .Verifiable(); var expander1Result = new[] { "some-seed" }; var expander1 = new Mock(); - expander1.Setup(e => e.PopulateValues(It.IsAny())) - .Callback((ViewLocationExpanderContext c) => - { - Assert.NotNull(c.ActionContext); - c.Values["expander-key"] = expander1.ToString(); - }) - .Verifiable(); - expander1.Setup(e => e.ExpandViewLocations(It.IsAny(), - It.IsAny>())) - .Callback((ViewLocationExpanderContext c, IEnumerable seeds) => - { - Assert.NotNull(c.ActionContext); - Assert.Equal(expectedSeeds, seeds); - }) - .Returns(expander1Result) - .Verifiable(); + expander1 + .Setup(e => e.PopulateValues(It.IsAny())) + .Callback((ViewLocationExpanderContext c) => + { + Assert.NotNull(c.ActionContext); + c.Values["expander-key"] = expander1.ToString(); + }) + .Verifiable(); + expander1 + .Setup(e => e.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .Callback((ViewLocationExpanderContext c, IEnumerable seeds) => + { + Assert.NotNull(c.ActionContext); + Assert.Equal(expectedSeeds, seeds); + }) + .Returns(expander1Result) + .Verifiable(); var expander2 = new Mock(); - expander2.Setup(e => e.ExpandViewLocations(It.IsAny(), - It.IsAny>())) - .Callback((ViewLocationExpanderContext c, IEnumerable seeds) => - { - Assert.Equal(expander1Result, seeds); - }) - .Returns(new[] { "test-string/{1}.cshtml" }) - .Verifiable(); + expander2 + .Setup(e => e.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .Callback((ViewLocationExpanderContext c, IEnumerable 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(); - pageFactory.Setup(p => p.CreateInstance("/Views/bar/baz.cshtml")) - .Verifiable(); - pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml")) - .Returns(Mock.Of()) - .Verifiable(); - - var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) - .Returns(Mock.Of()); - - var cache = GetViewLocationCache(); - var cacheMock = Mock.Get(cache); - cacheMock.Setup(c => c.Set(It.IsAny(), It.IsAny())) - .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(); + var pageFactory = new Mock(); + 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(result1.View); + Assert.Same(page, view1.RazorPage); + pageFactory.Verify(); + + // Act 2 + pageFactory + .Setup(p => p.CreateFactory(It.IsAny())) + .Throws(new Exception("Shouldn't be called")); + + var result2 = viewEngine.FindView(context, "baz"); + + // Assert 2 + Assert.True(result2.Success); + var view2 = Assert.IsType(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(MockBehavior.Strict); - pageFactory.Setup(p => p.CreateInstance("some-view-location")) - .Returns(Mock.Of()) - .Verifiable(); + var page1 = Mock.Of(); + var page2 = Mock.Of(); + var sequence = new MockSequence(); + var cancellationTokenSource = new CancellationTokenSource(); + var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); - var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) - .Returns(Mock.Of()); + var pageFactory = new Mock(); + 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(MockBehavior.Strict); - expander.Setup(v => v.PopulateValues(It.IsAny())) - .Verifiable(); - var cacheMock = new Mock(); - cacheMock.Setup(c => c.Get(It.IsAny())) - .Returns(new ViewLocationCacheResult("some-view-location", Enumerable.Empty())) - .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(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(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(); - pageFactory.Setup(p => p.CreateInstance("expired-location")) - .Returns((IRazorPage)null) - .Verifiable(); - pageFactory.Setup(p => p.CreateInstance("some-view-location")) - .Returns(Mock.Of()) - .Verifiable(); + var page1 = Mock.Of(); + var page2 = Mock.Of(); + var viewStart = Mock.Of(); + var sequence = new MockSequence(); + var cancellationTokenSource = new CancellationTokenSource(); + var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); - var viewFactory = new Mock(); - viewFactory.Setup(p => p.GetView(It.IsAny(), It.IsAny(), false)) - .Returns(Mock.Of()); + var pageFactory = new Mock(); + 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(); - cacheMock.Setup(c => c.Get(It.IsAny())) - .Returns(new ViewLocationCacheResult("expired-location", Enumerable.Empty())); - - var expander = new Mock(); - expander.Setup(v => v.PopulateValues(It.IsAny())) - .Verifiable(); - var expanderResult = new[] { "some-view-location" }; - expander.Setup(v => v.ExpandViewLocations( - It.IsAny(), It.IsAny>())) - .Returns((ViewLocationExpanderContext c, IEnumerable 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(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(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(); - var viewFactory = Mock.Of(); - var cache = new DefaultViewLocationCache(); + var pageFactory = Mock.Of(); var expander = new Mock(); var expandedLocations = new[] { @@ -548,16 +635,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test .Verifiable(); expander .Setup(v => v.ExpandViewLocations( - It.IsAny(), It.IsAny>())) - .Returns((ViewLocationExpanderContext c, IEnumerable viewLocations) => expandedLocations) + It.IsAny(), + It.IsAny>())) + .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(); + var pageFactory = new Mock(); + pageFactory + .Setup(p => p.CreateFactory("viewlocation3")) + .Returns(new RazorPageFactoryResult(new[] { changeToken })); + var expander = new Mock(); + var expandedLocations = new[] + { + "viewlocation1", + "viewlocation2", + "viewlocation3", + }; + expander + .Setup(v => v.PopulateValues(It.IsAny())) + .Callback((ViewLocationExpanderContext expanderContext) => + { + expanderContext.Values["somekey"] = "somevalue"; + }) + .Verifiable(); + expander + .Setup(v => v.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .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(result.View); + Assert.Same(page, view.RazorPage); + expander.Verify( + v => v.PopulateValues(It.IsAny()), + Times.Exactly(2)); + expander.Verify( + v => v.ExpandViewLocations(It.IsAny(), It.IsAny>()), + Times.Exactly(2)); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -604,36 +753,39 @@ namespace Microsoft.AspNet.Mvc.Razor.Test { // Arrange var page = Mock.Of(); - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("expanded-path/bar-layout")) - .Returns(page) - .Verifiable(); - - var viewFactory = new Mock(MockBehavior.Strict); + var pageFactory = new Mock(); + pageFactory + .Setup(p => p.CreateFactory("expanded-path/bar-layout")) + .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Verifiable(); var expander = new Mock(); - expander.Setup(e => e.PopulateValues(It.IsAny())) - .Callback((ViewLocationExpanderContext c) => - { - Assert.NotNull(c.ActionContext); - c.Values["expander-key"] = expander.ToString(); - }) - .Verifiable(); - expander.Setup(e => e.ExpandViewLocations(It.IsAny(), - It.IsAny>())) - .Returns((ViewLocationExpanderContext c, IEnumerable seeds) => - { - Assert.NotNull(c.ActionContext); - Assert.Equal(expectedSeeds, seeds); + expander + .Setup(e => e.PopulateValues(It.IsAny())) + .Callback((ViewLocationExpanderContext c) => + { + Assert.NotNull(c.ActionContext); + c.Values["expander-key"] = expander.ToString(); + }) + .Verifiable(); + expander + .Setup(e => e.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .Returns((ViewLocationExpanderContext c, IEnumerable 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(MockBehavior.Strict).Object; - var pageFactory = new Mock(); - pageFactory.Setup(p => p.CreateInstance("/Views/Foo/details.cshtml")) - .Returns(page) - .Verifiable(); + var pageFactory = new Mock(); + 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() @@ -1092,23 +1245,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test } } - private RazorViewEngine CreateViewEngine( - IRazorPageFactory pageFactory = null, - IRazorViewFactory viewFactory = null, - IEnumerable expanders = null, - IViewLocationCache cache = null) + private TestableRazorViewEngine CreateViewEngine( + IRazorPageFactoryProvider pageFactory = null, + IEnumerable expanders = null) { - pageFactory = pageFactory ?? Mock.Of(); - viewFactory = viewFactory ?? Mock.Of(); - - cache = cache ?? GetViewLocationCache(); - - var viewEngine = new RazorViewEngine(pageFactory, - viewFactory, - GetOptionsAccessor(expanders), - cache); - - return viewEngine; + pageFactory = pageFactory ?? Mock.Of(); + return new TestableRazorViewEngine( + pageFactory, + GetOptionsAccessor(expanders)); } private static IOptions GetOptionsAccessor( @@ -1129,15 +1273,6 @@ namespace Microsoft.AspNet.Mvc.Razor.Test return optionsAccessor.Object; } - private static IViewLocationCache GetViewLocationCache() - { - var cacheMock = new Mock(); - cacheMock.Setup(c => c.Get(It.IsAny())) - .Returns(null); - - return cacheMock.Object; - } - private static ActionContext GetActionContext(IDictionary 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 optionsAccessor, - IViewLocationCache cache) - : base(pageFactory, viewFactory, optionsAccessor, cache) + private IEnumerable _viewLocationFormats; + private IEnumerable _areaViewLocationFormats; + + public TestableRazorViewEngine( + IRazorPageFactoryProvider pageFactory, + IOptions optionsAccessor) + : base(pageFactory, Mock.Of(), new HtmlTestEncoder(), optionsAccessor) { } - public override IEnumerable ViewLocationFormats + public void SetLocationFormats( + IEnumerable viewLocationFormats, + IEnumerable areaViewLocationFormats) { - get - { - return new[] { "fake-path1/{1}/{0}.rzr" }; - } + _viewLocationFormats = viewLocationFormats; + _areaViewLocationFormats = areaViewLocationFormats; } - public override IEnumerable AreaViewLocationFormats - { - get - { - return new[] { "fake-area-path/{2}/{1}/{0}.rzr" }; - } - } + public override IEnumerable ViewLocationFormats => + _viewLocationFormats != null ? _viewLocationFormats : base.ViewLocationFormats; + + public override IEnumerable AreaViewLocationFormats => + _areaViewLocationFormats != null ? _areaViewLocationFormats : base.AreaViewLocationFormats; + + public IMemoryCache ViewLookupCachePublic => ViewLookupCache; } } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs deleted file mode 100644 index c0ae1805d3..0000000000 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewFactoryTest.cs +++ /dev/null @@ -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(), - Mock.Of(), - new HtmlTestEncoder()); - var page = Mock.Of(); - var viewEngine = Mock.Of(); - - // Act - var view = factory.GetView(viewEngine, page, isPartial); - - // Assert - var razorView = Assert.IsType(view); - Assert.Same(page, razorView.RazorPage); - Assert.Equal(razorView.IsPartial, isPartial); - } - - [Fact] - public void GetView_SetsRazorPage() - { - // Arrange - var factory = new RazorViewFactory( - Mock.Of(), - Mock.Of(), - new HtmlTestEncoder()); - - var page = Mock.Of(); - var viewEngine = Mock.Of(); - - // Act - var view = factory.GetView(viewEngine, page, isPartial: false); - - // Assert - Assert.NotNull(view); - var razorView = Assert.IsType(view); - Assert.Same(razorView.RazorPage, page); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 5a920ca219..4491262b9b 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -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(), - Mock.Of(), - CreateViewStartProvider(), - page, - new HtmlTestEncoder(), - isPartial: true); + var view = new RazorView( + Mock.Of(), + Mock.Of(), + 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(); - var view = new RazorView(Mock.Of(), - activator.Object, - CreateViewStartProvider(), - page, - new HtmlTestEncoder(), - isPartial: true); + var view = new RazorView( + Mock.Of(), + 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(); viewEngine.Setup(v => v.FindPage(It.IsAny(), 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(); activator.Setup(a => a.Activate(page, It.IsAny())) .Verifiable(); - var view = new RazorView(Mock.Of(), - activator.Object, - CreateViewStartProvider(), - page, - new HtmlTestEncoder(), - isPartial: true); + var view = new RazorView( + Mock.Of(), + 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(); - pageFactory.Setup(p => p.CreateInstance(LayoutPath)) - .Returns(layout); + var pageFactory = new Mock(); + pageFactory + .Setup(p => p.CreateFactory(LayoutPath)) + .Returns(new RazorPageFactoryResult(() => layout, new IChangeToken[0])); var viewEngine = new Mock(); viewEngine.Setup(v => v.FindPage(It.IsAny(), LayoutPath)) .Returns(new RazorPageResult(LayoutPath, layout)); - var viewStartProvider = CreateViewStartProvider(); - var view = new RazorView(viewEngine.Object, - Mock.Of(), - viewStartProvider, - page, - new HtmlTestEncoder(), - isPartial: true); + var view = new RazorView( + viewEngine.Object, + Mock.Of(), + 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()), 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(), - Mock.Of(), - CreateViewStartProvider(), - page, - new HtmlTestEncoder(), - isPartial: false); + var view = new RazorView( + Mock.Of(), + Mock.Of(), + 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(), - Mock.Of(), - CreateViewStartProvider(), - page, - new HtmlTestEncoder(), - isPartial: false); + var view = new RazorView( + Mock.Of(), + Mock.Of(), + 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(); activator.Setup(a => a.Activate(page, It.IsAny())) .Verifiable(); - var view = new RazorView(Mock.Of(), - activator.Object, - CreateViewStartProvider(), - page, - new HtmlTestEncoder(), - isPartial: false); + var view = new RazorView( + Mock.Of(), + 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())) .Verifiable(); - var view = new RazorView(Mock.Of(), - activator.Object, - CreateViewStartProvider(viewStart1, viewStart2), - page, - new HtmlTestEncoder(), - isPartial: false); + var view = new RazorView( + Mock.Of(), + 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(); var view = new RazorView(viewEngine.Object, Mock.Of(), - Mock.Of(), + 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - 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(), - CreateViewStartProvider(), + new IRazorPage[0], page, new HtmlTestEncoder(), isPartial: false); @@ -1089,7 +1096,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), Mock.Of(), - 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(), - CreateViewStartProvider(), + new IRazorPage[0], page, new HtmlTestEncoder(), isPartial: false); @@ -1204,13 +1211,9 @@ namespace Microsoft.AspNet.Mvc.Razor var viewEngine = new Mock(); viewEngine.Setup(p => p.FindPage(It.IsAny(), "Layout")) .Returns(new RazorPageResult("Layout", layout)); - var viewStartProvider = new Mock(); - viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny())) - .Returns(Enumerable.Empty()) - .Verifiable(); var view = new RazorView(viewEngine.Object, Mock.Of(), - 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(), Mock.Of(), - Mock.Of(), + new IRazorPage[0], page, new HtmlTestEncoder(), isPartial: true); @@ -1288,7 +1290,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), Mock.Of(), - Mock.Of(), + new IRazorPage[0], page, new HtmlTestEncoder(), isPartial); @@ -1328,7 +1330,7 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(viewEngine, Mock.Of(), - CreateViewStartProvider(viewStart1, viewStart2), + new[] { viewStart1, viewStart2 }, page, new HtmlTestEncoder(), isPartial: false); @@ -1364,12 +1366,13 @@ namespace Microsoft.AspNet.Mvc.Razor }); var viewEngine = Mock.Of(); - var view = new RazorView(viewEngine, - Mock.Of(), - CreateViewStartProvider(viewStart1, viewStart2), - page, - new HtmlTestEncoder(), - isPartial: false); + var view = new RazorView( + viewEngine, + Mock.Of(), + 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(); - viewEngine.Setup(p => p.FindPage(It.IsAny(), "/Layout.cshtml")) - .Returns(new RazorPageResult("Layout", layout)); + viewEngine + .Setup(p => p.FindPage(It.IsAny(), "/Layout.cshtml")) + .Returns(new RazorPageResult("Layout", layout)); var view = new RazorView( viewEngine.Object, Mock.Of(), - CreateViewStartProvider(viewStart), + new[] { viewStart }, page, new HtmlTestEncoder(), isPartial: false); @@ -1434,7 +1438,7 @@ namespace Microsoft.AspNet.Mvc.Razor }); var view = new RazorView(Mock.Of(), Mock.Of(), - 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(); + + var view = new RazorView( + viewEngine, + Mock.Of(), + 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(); @@ -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(); - viewStartProvider - .Setup(v => v.GetViewStartPages(It.IsAny())) - .Returns(viewStartPages); - - return viewStartProvider.Object; - } - private class TestableRazorPage : RazorPage { private readonly Action _executeAction;