diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 0a44ad1b26..232013d5d8 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.Text; using Microsoft.AspNet.FileProviders; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNet.Mvc.Razor.Compilation { @@ -54,9 +55,10 @@ public CompilerCache( throw new ArgumentNullException(nameof(precompiledViews)); } + var expirationTokens = new IChangeToken[0]; foreach (var item in precompiledViews) { - var cacheEntry = new CompilerCacheResult(CompilationResult.Successful(item.Value)); + var cacheEntry = new CompilerCacheResult(CompilationResult.Successful(item.Value), expirationTokens); _cache.Set(GetNormalizedPath(item.Key), cacheEntry); } } @@ -95,17 +97,18 @@ private CompilerCacheResult CreateCacheEntry( string normalizedPath, Func compile) { - CompilerCacheResult cacheResult; var fileInfo = _fileProvider.GetFileInfo(normalizedPath); MemoryCacheEntryOptions cacheEntryOptions; + CompilerCacheResult cacheResult; CompilerCacheResult cacheResultToCache; if (!fileInfo.Exists) { - cacheResultToCache = CompilerCacheResult.FileNotFound; - cacheResult = CompilerCacheResult.FileNotFound; + var expirationToken = _fileProvider.Watch(normalizedPath); + cacheResult = new CompilerCacheResult(new[] { expirationToken }); + cacheResultToCache = cacheResult; cacheEntryOptions = new MemoryCacheEntryOptions(); - cacheEntryOptions.AddExpirationToken(_fileProvider.Watch(normalizedPath)); + cacheEntryOptions.AddExpirationToken(expirationToken); } else { @@ -117,8 +120,11 @@ private CompilerCacheResult CreateCacheEntry( // 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); + CompilationResult.Successful(compilationResult.CompiledType), + cacheEntryOptions.ExpirationTokens); + cacheResult = new CompilerCacheResult( + compilationResult, + cacheEntryOptions.ExpirationTokens); } _cache.Set(normalizedPath, cacheResultToCache, cacheEntryOptions); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs index 130ab74c15..fc12832bbc 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheResult.cs @@ -2,6 +2,8 @@ // 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 { @@ -10,32 +12,36 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation /// public class 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 - public CompilerCacheResult(CompilationResult compilationResult) + public CompilerCacheResult(CompilationResult compilationResult, IList expirationTokens) { if (compilationResult == null) { throw new ArgumentNullException(nameof(compilationResult)); } + if (expirationTokens == null) + { + throw new ArgumentNullException(nameof(expirationTokens)); + } + CompilationResult = compilationResult; + ExpirationTokens = expirationTokens; } - /// - /// Initializes a new instance of for a failed file lookup. - /// - protected CompilerCacheResult() + public CompilerCacheResult(IList expirationTokens) { + if (expirationTokens == null) + { + throw new ArgumentNullException(nameof(expirationTokens)); + } + + CompilationResult = null; + ExpirationTokens = expirationTokens; } /// @@ -43,5 +49,9 @@ protected CompilerCacheResult() /// /// This property is null when file lookup failed. public CompilationResult CompilationResult { get; } + + public IList ExpirationTokens { get; } + + public bool IsFoundResult => CompilationResult != null; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/DefaultRazorPageFactory.cs similarity index 77% rename from src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs rename to src/Microsoft.AspNet.Mvc.Razor/DefaultRazorPageFactory.cs index 93f1d60b22..7241736282 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/DefaultRazorPageFactory.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// Represents a that creates instances /// from razor files in the file system. /// - public class VirtualPathRazorPageFactory : IRazorPageFactory + public class DefaultRazorPageFactory : IRazorPageFactory { /// /// This delegate holds on to an instance of . @@ -24,7 +24,7 @@ public class VirtualPathRazorPageFactory : IRazorPageFactory /// /// The . /// The . - public VirtualPathRazorPageFactory( + public DefaultRazorPageFactory( IRazorCompilationService razorCompilationService, ICompilerCacheProvider compilerCacheProvider) { @@ -46,7 +46,7 @@ private ICompilerCache CompilerCache } /// - public IRazorPage CreateInstance(string relativePath) + public RazorPageFactoryResult CreateFactory(string relativePath) { if (relativePath == null) { @@ -58,18 +58,20 @@ public IRazorPage CreateInstance(string relativePath) // 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.IsFoundResult) { - return null; + return new RazorPageFactoryResult(() => + { + var page = (IRazorPage)Activator.CreateInstance(result.CompilationResult.CompiledType); + page.Path = relativePath; + return page; + }, result.ExpirationTokens); + } + else + { + return new RazorPageFactoryResult(result.ExpirationTokens); } - - var page = (IRazorPage)Activator.CreateInstance(result.CompilationResult.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 7bd621dd95..19353c55cc 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -131,8 +131,6 @@ internal static void AddRazorViewEngineServices(IServiceCollection services) 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>(); @@ -148,9 +146,8 @@ internal static void AddRazorViewEngineServices(IServiceCollection services) // 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/IRazorPageFactory.cs index b4cc1a011e..298163f14e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs @@ -9,10 +9,10 @@ namespace Microsoft.AspNet.Mvc.Razor public interface IRazorPageFactory { /// - /// 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 index c49731f8ae..c3bfede118 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using Microsoft.AspNet.Mvc.ViewEngines; namespace Microsoft.AspNet.Mvc.Razor @@ -18,6 +19,6 @@ public interface IRazorViewFactory /// 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); + IView GetView(IRazorViewEngine viewEngine, IRazorPage page, IReadOnlyList viewStarts, 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/RazorPageFactoryResult.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageFactoryResult.cs new file mode 100644 index 0000000000..9d448bffd3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageFactoryResult.cs @@ -0,0 +1,32 @@ +// 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 +{ + public struct RazorPageFactoryResult + { + public RazorPageFactoryResult(IList expirationTokens) + { + ExpirationTokens = expirationTokens; + RazorPageFactory = null; + } + + public RazorPageFactoryResult( + Func razorPageFactory, + IList expirationTokens) + { + RazorPageFactory = razorPageFactory; + ExpirationTokens = expirationTokens; + } + + public Func RazorPageFactory { get; } + + public IList ExpirationTokens { get; } + + public bool IsFoundResult => RazorPageFactory != null; + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs index 92628d18a5..8400d172cf 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageResult.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// Represents the results of locating a . /// - public class RazorPageResult + public struct RazorPageResult { /// /// Initializes a new instance of for a successful discovery. @@ -30,6 +30,7 @@ public RazorPageResult(string name, IRazorPage page) Name = name; Page = page; + SearchedLocations = null; } /// @@ -50,6 +51,7 @@ public RazorPageResult(string name, IEnumerable searchedLocations) } 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..d034c84cba 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -22,7 +22,7 @@ public class RazorView : IView { private readonly IRazorViewEngine _viewEngine; private readonly IRazorPageActivator _pageActivator; - private readonly IViewStartProvider _viewStartProvider; + private readonly IReadOnlyList _viewStartPages; private readonly HtmlEncoder _htmlEncoder; private IPageExecutionListenerFeature _pageExecutionFeature; @@ -31,7 +31,7 @@ public class RazorView : IView /// /// The used to locate Layout pages. /// The used to activate pages. - /// The used for discovery of _ViewStart + /// The used for discovery of _ViewStart /// The instance to execute. /// The HTML encoder. /// Determines if the view is to be executed as a partial. @@ -39,14 +39,14 @@ public class RazorView : IView 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; @@ -153,14 +153,13 @@ private Task RenderPageCoreAsync(IRazorPage page, ViewContext context) 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..f85fccb1bd 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -5,10 +5,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; 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 +23,30 @@ 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 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 const string ControllerKey = "controller"; + private const string AreaKey = "area"; + private static readonly ViewLocationCacheItem[] EmptyViewStartLocationCacheItems = + new ViewLocationCacheItem[0]; private readonly IRazorPageFactory _pageFactory; private readonly IRazorViewFactory _viewFactory; private readonly IList _viewLocationExpanders; - private readonly IViewLocationCache _viewLocationCache; /// - /// 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) + IOptions optionsAccessor) { _pageFactory = pageFactory; _viewFactory = viewFactory; _viewLocationExpanders = optionsAccessor.Value.ViewLocationExpanders; - _viewLocationCache = viewLocationCache; + ViewLookupCache = new MemoryCache(new MemoryCacheOptions + { + CompactOnMemoryPressure = false + }); } /// @@ -72,10 +62,11 @@ public RazorViewEngine( /// 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 +83,20 @@ public virtual IEnumerable ViewLocationFormats /// 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 +108,12 @@ public ViewEngineResult FindView( 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 +125,12 @@ public ViewEngineResult FindPartialView( 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 +142,16 @@ public RazorPageResult FindPage( throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(pageName)); } - return GetRazorPageResult(context, pageName, isPartial: true); + var cacheResult = GetViewLocationCacheResult(context, pageName, isPartial: true); + if (cacheResult.IsFoundResult) + { + var razorPage = cacheResult.ViewEntry.PageFactory(); + return new RazorPageResult(pageName, razorPage); + } + else + { + return new RazorPageResult(pageName, cacheResult.SearchedLocations); + } } /// @@ -166,9 +167,7 @@ public RazorPageResult FindPage( /// 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,7 +227,7 @@ public static string GetNormalizedRouteValue( return stringRouteValue; } - private RazorPageResult GetRazorPageResult( + private ViewLocationCacheResult GetViewLocationCacheResult( ActionContext context, string pageName, bool isPartial) @@ -241,13 +240,32 @@ private RazorPageResult GetRazorPageResult( applicationRelativePath += ViewExtension; } - var page = _pageFactory.CreateInstance(applicationRelativePath); - if (page != null) + var cacheKey = new ViewLocationCacheKey( + applicationRelativePath, + controllerName: null, + areaName: null, + isPartial: isPartial, + values: null); + + ViewLocationCacheResult cacheResult; + if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult)) { - return new RazorPageResult(pageName, page); + var expirationTokens = new HashSet(); + cacheResult = CreateCacheResult(cacheKey, expirationTokens, applicationRelativePath, isPartial); + + var cacheEntryOptions = new MemoryCacheEntryOptions(); + foreach (var expirationToken in expirationTokens) + { + cacheEntryOptions.AddExpirationToken(expirationToken); + } + + cacheResult = ViewLookupCache.Set( + cacheKey, + cacheResult, + cacheEntryOptions); } - return new RazorPageResult(pageName, new[] { pageName }); + return cacheResult; } else { @@ -255,105 +273,171 @@ private RazorPageResult GetRazorPageResult( } } - private RazorPageResult LocatePageFromViewLocations( - ActionContext context, + 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); - - // Only use the area view location formats if we have an area token. - var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats : - ViewLocationFormats; + var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey); + var areaName = GetNormalizedRouteValue(actionContext, AreaKey); + var expanderContext = new ViewLocationExpanderContext( + actionContext, + pageName, + controllerName, + areaName, + isPartial); + Dictionary expanderValues = null; - 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)) + var cacheKey = new ViewLocationCacheKey( + expanderContext.ViewName, + expanderContext.ControllerName, + expanderContext.ViewName, + expanderContext.IsPartial, + expanderValues); + + ViewLocationCacheResult cacheResult; + if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult)) { - if (cachedResult.IsFoundResult) - { - var page = _pageFactory.CreateInstance(cachedResult.ViewLocation); + cacheResult = OnCacheMiss(expanderContext, cacheKey); + } - if (page != null) - { - // 2a We have a cache entry where a view was previously found. - return new RazorPageResult(pageName, page); - } - } - else - { - locationsToSearch = cachedResult.SearchedLocations; - } + 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++) + { + viewLocations = _viewLocationExpanders[i].ExpandViewLocations(expanderContext, viewLocations); } - if (locationsToSearch == null) + ViewLocationCacheResult cacheResult = null; + var searchedLocations = new List(); + var expirationTokens = new HashSet(); + foreach (var location in viewLocations) { - // 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) + var path = string.Format( + CultureInfo.InvariantCulture, + location, + expanderContext.ViewName, + expanderContext.ControllerName, + expanderContext.AreaName); + + cacheResult = CreateCacheResult(cacheKey, expirationTokens, path, expanderContext.IsPartial); + if (cacheResult != null) { - viewLocations = expander.ExpandViewLocations(expanderContext, viewLocations); + break; } - var controllerName = GetNormalizedRouteValue(context, ControllerKey); + searchedLocations.Add(path); + } - locationsToSearch = viewLocations.Select( - location => string.Format( - CultureInfo.InvariantCulture, - location, - pageName, - controllerName, - areaName - )); + // No views were found at the specified location. Create a not found result. + if (cacheResult == null) + { + cacheResult = new ViewLocationCacheResult(searchedLocations); } - // 3. Use the expanded locations to look up a page. - var searchedLocations = new List(); - foreach (var path in locationsToSearch) + var cacheEntryOptions = new MemoryCacheEntryOptions(); + foreach (var expirationToken in expirationTokens) { - var page = _pageFactory.CreateInstance(path); - if (page != null) + 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); + for (var i = 0; i < factoryResult.ExpirationTokens.Count; i++) + { + expirationTokens.Add(factoryResult.ExpirationTokens[i]); + } + + if (factoryResult.IsFoundResult) + { + // 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); + for (var i = 0; i < result.ExpirationTokens.Count; i++) { - // 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); + expirationTokens.Add(result.ExpirationTokens[i]); } - searchedLocations.Add(path); + if (result.IsFoundResult) + { + // 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)); + } } - // 3b. We did not find a page for any of the paths. - _viewLocationCache.Set(expanderContext, new ViewLocationCacheResult(searchedLocations)); - return new RazorPageResult(pageName, searchedLocations); + return viewStartPages; } private ViewEngineResult CreateViewEngineResult( - RazorPageResult result, - IRazorViewFactory razorViewFactory, + ViewLocationCacheResult result, + string viewName, bool isPartial) { - if (result.SearchedLocations != null) + if (!result.IsFoundResult) + { + return ViewEngineResult.NotFound(viewName, result.SearchedLocations); + } + + var page = result.ViewEntry.PageFactory(); + var viewStarts = new IRazorPage[result.ViewStartEntries.Count]; + for (var i = 0; i < viewStarts.Length; i++) { - return ViewEngineResult.NotFound(result.Name, result.SearchedLocations); + var viewStartItem = result.ViewStartEntries[i]; + viewStarts[i] = result.ViewStartEntries[i].PageFactory(); } - var view = razorViewFactory.GetView(this, result.Page, isPartial); - return ViewEngineResult.Found(result.Name, view); + var view = _viewFactory.GetView(this, page, viewStarts, 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 index 661926a481..fed57cadff 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewFactory.cs @@ -2,6 +2,7 @@ // 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.Text.Encodings.Web; using Microsoft.AspNet.Mvc.ViewEngines; @@ -15,7 +16,6 @@ public class RazorViewFactory : IRazorViewFactory { private readonly HtmlEncoder _htmlEncoder; private readonly IRazorPageActivator _pageActivator; - private readonly IViewStartProvider _viewStartProvider; /// /// Initializes a new instance of RazorViewFactory @@ -25,11 +25,9 @@ public class RazorViewFactory : IRazorViewFactory /// pages public RazorViewFactory( IRazorPageActivator pageActivator, - IViewStartProvider viewStartProvider, HtmlEncoder htmlEncoder) { _pageActivator = pageActivator; - _viewStartProvider = viewStartProvider; _htmlEncoder = htmlEncoder; } @@ -37,6 +35,7 @@ public RazorViewFactory( public IView GetView( IRazorViewEngine viewEngine, IRazorPage page, + IReadOnlyList viewStartPages, bool isPartial) { if (viewEngine == null) @@ -52,7 +51,7 @@ public IView GetView( var razorView = new RazorView( viewEngine, _pageActivator, - _viewStartProvider, + viewStartPages, page, _htmlEncoder, isPartial); diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheItem.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheItem.cs new file mode 100644 index 0000000000..328add29bb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheItem.cs @@ -0,0 +1,17 @@ +using System; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public struct ViewLocationCacheItem + { + public ViewLocationCacheItem(Func razorPageFactory, string location) + { + PageFactory = razorPageFactory; + Location = location; + } + + public string Location { get; } + + 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..89c8ed9b3a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheKey.cs @@ -0,0 +1,91 @@ +// 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 +{ + public struct ViewLocationCacheKey : IEquatable + { + public ViewLocationCacheKey( + string viewName, + string controllerName, + string areaName, + bool isPartial, + IReadOnlyDictionary values) + { + ViewName = viewName; + ControllerName = controllerName; + AreaName = areaName; + IsPartial = isPartial; + ViewLocationExpanderValues = values; + } + + public string ViewName { get; } + + public string ControllerName { get; } + + public string AreaName { get; } + + public bool IsPartial { get; } + + 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 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.ViewLocationExpanderValues != null) + { + foreach (var item in key.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..dacaa46888 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationCacheResult.cs @@ -3,15 +3,13 @@ using System; using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNet.Mvc.Razor { /// /// Result of lookups. /// - public struct ViewLocationCacheResult : IEquatable + public class ViewLocationCacheResult { /// /// Initializes a new instance of @@ -21,17 +19,16 @@ public struct ViewLocationCacheResult : IEquatable /// Locations that were searched /// in addition to . 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; + ViewEntry = view; + ViewStartEntries = viewStarts; IsFoundResult = true; } @@ -48,20 +45,11 @@ public ViewLocationCacheResult(IEnumerable searchedLocations) } SearchedLocations = searchedLocations; - ViewLocation = null; - IsFoundResult = false; } - /// - /// A that represents a cache miss. - /// - public static readonly ViewLocationCacheResult None = new ViewLocationCacheResult(Enumerable.Empty()); + public ViewLocationCacheItem ViewEntry { get; } - /// - /// The location the view was found. - /// - /// This is available if is true. - public string ViewLocation { get; } + public IReadOnlyList ViewStartEntries { get; } /// /// The sequence of locations that were searched. @@ -77,54 +65,5 @@ public ViewLocationCacheResult(IEnumerable searchedLocations) /// 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; - } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs index 35c4f6bc04..8e4845867b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewLocationExpanderContext.cs @@ -20,6 +20,8 @@ public class ViewLocationExpanderContext public ViewLocationExpanderContext( ActionContext actionContext, string viewName, + string controllerName, + string areaName, bool isPartial) { if (actionContext == null) @@ -34,6 +36,8 @@ public ViewLocationExpanderContext( ActionContext = actionContext; ViewName = viewName; + ControllerName = controllerName; + AreaName = areaName; IsPartial = isPartial; } @@ -47,6 +51,10 @@ public ViewLocationExpanderContext( /// public string ViewName { get; } + public string ControllerName { get; } + + 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