diff --git a/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs b/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs index 0c5cb43c2a..c0f65820f9 100644 --- a/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs @@ -516,6 +516,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.HandlerMethodDescriptor Select(Microsoft.AspNetCore.Mvc.RazorPages.PageContext context); } + [System.ObsoleteAttribute("This type is obsolete. Use PageLoader instead.")] public partial interface IPageLoader { Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor Load(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor); @@ -534,6 +535,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure System.Reflection.PropertyInfo Microsoft.AspNetCore.Mvc.Infrastructure.IPropertyInfoParameterDescriptor.PropertyInfo { get { throw null; } } public System.Reflection.PropertyInfo Property { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } + public abstract partial class PageLoader : Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.IPageLoader + { + protected PageLoader() { } + public abstract System.Threading.Tasks.Task LoadAsync(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor); + Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.IPageLoader.Load(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor) { throw null; } + } [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=false, Inherited=true)] public partial class PageModelAttribute : System.Attribute { diff --git a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index aca9562661..d5c92d94a1 100644 --- a/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -116,7 +116,10 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); +#pragma warning disable CS0618 // Type or member is obsolete + services.TryAddSingleton(s => s.GetRequiredService()); +#pragma warning restore CS0618 // Type or member is obsolete + services.TryAddSingleton(); services.TryAddSingleton(); // Action executors diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs index 425bd1ab15..dbf25a406d 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs @@ -2,37 +2,45 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { - internal class DefaultPageLoader : IPageLoader + internal class DefaultPageLoader : PageLoader { + private readonly IActionDescriptorCollectionProvider _collectionProvider; private readonly IPageApplicationModelProvider[] _applicationModelProviders; private readonly IViewCompilerProvider _viewCompilerProvider; private readonly ActionEndpointFactory _endpointFactory; private readonly PageConventionCollection _conventions; private readonly FilterCollection _globalFilters; + private volatile InnerCache _currentCache; public DefaultPageLoader( + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IEnumerable applicationModelProviders, IViewCompilerProvider viewCompilerProvider, ActionEndpointFactory endpointFactory, IOptions pageOptions, IOptions mvcOptions) { + _collectionProvider = actionDescriptorCollectionProvider; _applicationModelProviders = applicationModelProviders .OrderBy(p => p.Order) .ToArray(); + _viewCompilerProvider = viewCompilerProvider; _endpointFactory = endpointFactory; _conventions = pageOptions.Value.Conventions; @@ -41,16 +49,42 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); - public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor) + private ConcurrentDictionary> CurrentCache + { + get + { + var current = _currentCache; + var actionDescriptors = _collectionProvider.ActionDescriptors; + + if (current == null || current.Version != actionDescriptors.Version) + { + current = new InnerCache(actionDescriptors.Version); + _currentCache = current; + } + + return current.Entries; + } + } + + public override Task LoadAsync(PageActionDescriptor actionDescriptor) { if (actionDescriptor == null) { throw new ArgumentNullException(nameof(actionDescriptor)); } - var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); - var viewDescriptor = compileTask.GetAwaiter().GetResult(); + var cache = CurrentCache; + if (cache.TryGetValue(actionDescriptor, out var compiledDescriptorTask)) + { + return compiledDescriptorTask; + } + return cache.GetOrAdd(actionDescriptor, LoadAsyncCore(actionDescriptor)); + } + + private async Task LoadAsyncCore(PageActionDescriptor actionDescriptor) + { + var viewDescriptor = await Compiler.CompileAsync(actionDescriptor.RelativePath); var context = new PageApplicationModelProviderContext(actionDescriptor, viewDescriptor.Type.GetTypeInfo()); for (var i = 0; i < _applicationModelProviders.Length; i++) { @@ -65,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure ApplyConventions(_conventions, context.PageApplicationModel); var compiled = CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel, _globalFilters); - + // We need to create an endpoint for routing to use and attach it to the CompiledPageActionDescriptor... // routing for pages is two-phase. First we perform routing using the route info - we can do this without // compiling/loading the page. Then once we have a match we load the page and we can create an endpoint @@ -128,5 +162,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure attributes.OfType()); } } + + private sealed class InnerCache + { + public InnerCache(int version) + { + Version = version; + Entries = new ConcurrentDictionary>(); + } + + public ConcurrentDictionary> Entries { get; } + + public int Version { get; } + } } } \ No newline at end of file diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/IPageLoader.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/IPageLoader.cs index 5d9a9cc0c4..e1d87515b5 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/IPageLoader.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/IPageLoader.cs @@ -1,11 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { /// /// Creates a from a . /// + [Obsolete("This type is obsolete. Use " + nameof(PageLoader) + " instead.")] public interface IPageLoader { /// diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs index 3747076656..d163b5564b 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -19,9 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { internal class PageActionInvokerProvider : IActionInvokerProvider { - private const string ViewStartFileName = "_ViewStart.cshtml"; - - private readonly IPageLoader _loader; + private readonly PageLoader _loader; private readonly IPageFactoryProvider _pageFactoryProvider; private readonly IPageModelFactoryProvider _modelFactoryProvider; private readonly IModelBinderFactory _modelBinderFactory; @@ -43,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private volatile InnerCache _currentCache; public PageActionInvokerProvider( - IPageLoader loader, + PageLoader loader, IPageFactoryProvider pageFactoryProvider, IPageModelFactoryProvider modelFactoryProvider, IRazorPageFactoryProvider razorPageFactoryProvider, @@ -81,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } public PageActionInvokerProvider( - IPageLoader loader, + PageLoader loader, IPageFactoryProvider pageFactoryProvider, IPageModelFactoryProvider modelFactoryProvider, IRazorPageFactoryProvider razorPageFactoryProvider, @@ -139,7 +138,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure IFilterMetadata[] filters; if (!cache.Entries.TryGetValue(actionDescriptor, out var cacheEntry)) { - actionContext.ActionDescriptor = _loader.Load(actionDescriptor); + CompiledPageActionDescriptor compiledPageActionDescriptor; + if (_mvcOptions.EnableEndpointRouting) + { + // With endpoint routing, PageLoaderMatcherPolicy should have already produced a CompiledPageActionDescriptor. + compiledPageActionDescriptor = (CompiledPageActionDescriptor)actionDescriptor; + } + else + { + // With legacy routing, we're forced to perform a blocking call. The exceptation is that + // in the most common case - build time views or successsively cached runtime views - this should finish synchronously. + compiledPageActionDescriptor = _loader.LoadAsync(actionDescriptor).GetAwaiter().GetResult(); + } + + actionContext.ActionDescriptor = compiledPageActionDescriptor; var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, actionContext); filters = filterFactoryResult.Filters; @@ -285,7 +297,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private PageHandlerBinderDelegate[] GetHandlerBinders(CompiledPageActionDescriptor actionDescriptor) { - if (actionDescriptor.HandlerMethods == null ||actionDescriptor.HandlerMethods.Count == 0) + if (actionDescriptor.HandlerMethods == null || actionDescriptor.HandlerMethods.Count == 0) { return Array.Empty(); } diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoader.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoader.cs new file mode 100644 index 0000000000..f2e914ddf5 --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoader.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + /// + /// Creates a from a . + /// +#pragma warning disable CS0618 // Type or member is obsolete + public abstract class PageLoader : IPageLoader +#pragma warning restore CS0618 // Type or member is obsolete + { + /// + /// Produces a given a . + /// + /// The . + /// A that on completion returns a . + public abstract Task LoadAsync(PageActionDescriptor actionDescriptor); + + CompiledPageActionDescriptor IPageLoader.Load(PageActionDescriptor actionDescriptor) + => LoadAsync(actionDescriptor).GetAwaiter().GetResult(); + } +} diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs index d41a445b48..6ff7ceb1c9 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs @@ -7,15 +7,14 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { internal class PageLoaderMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy { - private readonly IPageLoader _loader; + private readonly PageLoader _loader; - public PageLoaderMatcherPolicy(IPageLoader loader) + public PageLoaderMatcherPolicy(PageLoader loader) { if (loader == null) { @@ -69,21 +68,51 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { throw new ArgumentNullException(nameof(candidates)); } - + for (var i = 0; i < candidates.Count; i++) { ref var candidate = ref candidates[i]; - var endpoint = (RouteEndpoint)candidate.Endpoint; + var endpoint = candidate.Endpoint; var page = endpoint.Metadata.GetMetadata(); if (page != null) { - var compiled = _loader.Load(page); - candidates.ReplaceEndpoint(i, compiled.Endpoint, candidate.Values); + // We found an endpoint instance that has a PageActionDescriptor, but not a + // CompiledPageActionDescriptor. Update the CandidateSet. + var compiled = _loader.LoadAsync(page); + if (compiled.IsCompletedSuccessfully) + { + candidates.ReplaceEndpoint(i, compiled.Result.Endpoint, candidate.Values); + } + else + { + // In the most common case, GetOrAddAsync will return a synchronous result. + // Avoid going async since this is a fairly hot path. + return ApplyAsyncAwaited(candidates, compiled, i); + } } } return Task.CompletedTask; } + + private async Task ApplyAsyncAwaited(CandidateSet candidates, Task actionDescriptorTask, int index) + { + var compiled = await actionDescriptorTask; + candidates.ReplaceEndpoint(index, compiled.Endpoint, candidates[index].Values); + + for (var i = index + 1; i < candidates.Count; i++) + { + var candidate = candidates[i]; + var endpoint = candidate.Endpoint; + + var page = endpoint.Metadata.GetMetadata(); + if (page != null) + { + compiled = await _loader.LoadAsync(page); + candidates.ReplaceEndpoint(i, compiled.Endpoint, candidates[i].Values); + } + } + } } } diff --git a/src/Mvc/Mvc.RazorPages/src/PageActionDescriptor.cs b/src/Mvc/Mvc.RazorPages/src/PageActionDescriptor.cs index eaa15ff9ef..57a2a17cb2 100644 --- a/src/Mvc/Mvc.RazorPages/src/PageActionDescriptor.cs +++ b/src/Mvc/Mvc.RazorPages/src/PageActionDescriptor.cs @@ -81,6 +81,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages } } - private string DebuggerDisplayString => $"{{ViewEnginePath = {nameof(ViewEnginePath)}, RelativePath = {nameof(RelativePath)}}}"; + private string DebuggerDisplayString => $"{nameof(ViewEnginePath)} = {ViewEnginePath}, {nameof(RelativePath)} = {RelativePath}"; } } \ No newline at end of file diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs index b9c0b8462e..2177cf64d7 100644 --- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DefaultPageLoaderTest.cs @@ -3,6 +3,8 @@ using System; using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.Compilation; @@ -18,8 +20,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageLoaderTest { + private readonly IActionDescriptorCollectionProvider ActionDescriptorCollectionProvider; + + public DefaultPageLoaderTest() + { + var actionDescriptors = new ActionDescriptorCollection(Array.Empty(), 1); + ActionDescriptorCollectionProvider = Mock.Of(v => v.ActionDescriptors == actionDescriptors); + } + [Fact] - public void Load_InvokesApplicationModelProviders() + public async Task LoadAsync_InvokesApplicationModelProviders() { // Arrange var descriptor = new PageActionDescriptor(); @@ -77,6 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure }; var loader = new DefaultPageLoader( + ActionDescriptorCollectionProvider, providers, compilerProvider, endpointFactory, @@ -84,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure mvcOptions); // Act - var result = loader.Load(new PageActionDescriptor()); + var result = await loader.LoadAsync(new PageActionDescriptor()); // Assert provider1.Verify(); @@ -92,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } [Fact] - public void Load_CreatesEndpoint_WithRoute() + public async Task LoadAsync_CreatesEndpoint_WithRoute() { // Arrange var descriptor = new PageActionDescriptor() @@ -132,6 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure }; var loader = new DefaultPageLoader( + ActionDescriptorCollectionProvider, providers, compilerProvider, endpointFactory, @@ -139,14 +151,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure mvcOptions); // Act - var result = loader.Load(descriptor); + var result = await loader.LoadAsync(descriptor); // Assert Assert.NotNull(result.Endpoint); } [Fact] - public void Load_InvokesApplicationModelProviders_WithTheRightOrder() + public async Task LoadAsync_InvokesApplicationModelProviders_WithTheRightOrder() { // Arrange var descriptor = new PageActionDescriptor(); @@ -196,6 +208,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure }; var loader = new DefaultPageLoader( + ActionDescriptorCollectionProvider, providers, compilerProvider, endpointFactory, @@ -203,13 +216,134 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure mvcOptions); // Act - var result = loader.Load(new PageActionDescriptor()); + var result = await loader.LoadAsync(new PageActionDescriptor()); // Assert provider1.Verify(); provider2.Verify(); } + [Fact] + public async Task LoadAsync_CachesResults() + { + // Arrange + var descriptor = new PageActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + }; + + var transformer = new Mock(); + transformer + .Setup(t => t.SubstituteRequiredValues(It.IsAny(), It.IsAny())) + .Returns((p, v) => p); + + var compilerProvider = GetCompilerProvider(); + + var razorPagesOptions = Options.Create(new RazorPagesOptions()); + var mvcOptions = Options.Create(new MvcOptions()); + var endpointFactory = new ActionEndpointFactory(transformer.Object); + + var provider = new Mock(); + + var pageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + + provider.Setup(p => p.OnProvidersExecuting(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => + { + Assert.Null(c.PageApplicationModel); + c.PageApplicationModel = pageApplicationModel; + }) + .Verifiable(); + + var providers = new[] + { + provider.Object, + }; + + var loader = new DefaultPageLoader( + ActionDescriptorCollectionProvider, + providers, + compilerProvider, + endpointFactory, + razorPagesOptions, + mvcOptions); + + // Act + var result1 = await loader.LoadAsync(descriptor); + var result2 = await loader.LoadAsync(descriptor); + + // Assert + Assert.Same(result1, result2); + } + + [Fact] + public async Task LoadAsync_UpdatesResults() + { + // Arrange + var descriptor = new PageActionDescriptor() + { + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "/test", + }, + }; + + var transformer = new Mock(); + transformer + .Setup(t => t.SubstituteRequiredValues(It.IsAny(), It.IsAny())) + .Returns((p, v) => p); + + var compilerProvider = GetCompilerProvider(); + + var razorPagesOptions = Options.Create(new RazorPagesOptions()); + var mvcOptions = Options.Create(new MvcOptions()); + var endpointFactory = new ActionEndpointFactory(transformer.Object); + + var provider = new Mock(); + + var pageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + + provider.Setup(p => p.OnProvidersExecuting(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => + { + Assert.Null(c.PageApplicationModel); + c.PageApplicationModel = pageApplicationModel; + }) + .Verifiable(); + + var providers = new[] + { + provider.Object, + }; + + var descriptorCollection1 = new ActionDescriptorCollection(new[] { descriptor }, version: 1); + var descriptorCollection2 = new ActionDescriptorCollection(new[] { descriptor }, version: 2); + + var actionDescriptorCollectionProvider = new Mock(); + actionDescriptorCollectionProvider + .SetupSequence(p => p.ActionDescriptors) + .Returns(descriptorCollection1) + .Returns(descriptorCollection2); + + var loader = new DefaultPageLoader( + actionDescriptorCollectionProvider.Object, + providers, + compilerProvider, + endpointFactory, + razorPagesOptions, + mvcOptions); + + // Act + var result1 = await loader.LoadAsync(descriptor); + var result2 = await loader.LoadAsync(descriptor); + + // Assert + Assert.NotSame(result1, result2); + } + [Fact] public void ApplyConventions_InvokesApplicationModelConventions() { @@ -553,11 +687,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private static IViewCompilerProvider GetCompilerProvider() { - var descriptor = new CompiledViewDescriptor - { - Item = TestRazorCompiledItem.CreateForView(typeof(object), "/Views/Index.cshtml"), - }; - + var compiledItem = TestRazorCompiledItem.CreateForView(typeof(object), "/Views/Index.cshtml"); + var descriptor = new CompiledViewDescriptor(compiledItem); var compiler = new Mock(); compiler.Setup(c => c.CompileAsync(It.IsAny())) .ReturnsAsync(descriptor); diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionInvokerProviderTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionInvokerProviderTest.cs index 0133ce9ffb..550c66e8c7 100644 --- a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionInvokerProviderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageActionInvokerProviderTest.cs @@ -30,19 +30,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public void OnProvidersExecuting_WithEmptyModel_PopulatesCacheEntry() { // Arrange - var descriptor = new PageActionDescriptor + var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor { RelativePath = "/Path1", FilterDescriptors = new FilterDescriptor[0], - }; + }); Func factory = (a, b) => null; Action releaser = (a, b, c) => { }; - var loader = new Mock(); - loader - .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor)); + var loader = Mock.Of(); var pageFactoryProvider = new Mock(); pageFactoryProvider @@ -53,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure .Returns(releaser); var invokerProvider = CreateInvokerProvider( - loader.Object, + loader, CreateActionDescriptorCollection(descriptor), pageFactoryProvider.Object); @@ -83,25 +80,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public void OnProvidersExecuting_WithModel_PopulatesCacheEntry() { // Arrange - var descriptor = new PageActionDescriptor - { - RelativePath = "/Path1", - FilterDescriptors = new FilterDescriptor[0] - }; + var descriptor = CreateCompiledPageActionDescriptor( + new PageActionDescriptor + { + RelativePath = "/Path1", + FilterDescriptors = new FilterDescriptor[0] + }, + pageType: typeof(PageWithModel), + modelType: typeof(DerivedTestPageModel)); Func factory = (a, b) => null; Action releaser = (a, b, c) => { }; Func modelFactory = _ => null; Action modelDisposer = (_, __) => { }; - var loader = new Mock(); - loader - .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor( - descriptor, - pageType: typeof(PageWithModel), - modelType: typeof(DerivedTestPageModel))); - + var loader = Mock.Of(); var pageFactoryProvider = new Mock(); pageFactoryProvider .Setup(f => f.CreatePageFactory(It.IsAny())) @@ -119,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure .Returns(modelDisposer); var invokerProvider = CreateInvokerProvider( - loader.Object, + loader, CreateActionDescriptorCollection(descriptor), pageFactoryProvider.Object, modelFactoryProvider.Object); @@ -162,18 +155,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public void OnProvidersExecuting_CachesViewStartFactories() { // Arrange - var descriptor = new PageActionDescriptor + var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor { RelativePath = "/Home/Path1/File.cshtml", ViewEnginePath = "/Home/Path1/File.cshtml", FilterDescriptors = new FilterDescriptor[0], - }; - - var loader = new Mock(); - loader - .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel))); + }, pageType: typeof(PageWithModel)); + var loader = Mock.Of(); var razorPageFactoryProvider = new Mock(); Func factory1 = () => null; @@ -191,7 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure fileProvider.AddFile("/_ViewStart.cshtml", "content2"); var invokerProvider = CreateInvokerProvider( - loader.Object, + loader, CreateActionDescriptorCollection(descriptor), razorPageFactoryProvider: razorPageFactoryProvider.Object); @@ -216,19 +205,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public void OnProvidersExecuting_CachesEntries() { // Arrange - var descriptor = new PageActionDescriptor + var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor { RelativePath = "/Path1", FilterDescriptors = new FilterDescriptor[0], - }; + }); - var loader = new Mock(); - loader - .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor)); + var loader = Mock.Of(); var invokerProvider = CreateInvokerProvider( - loader.Object, + loader, CreateActionDescriptorCollection(descriptor)); var context = new ActionInvokerProviderContext(new ActionContext @@ -263,7 +249,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } [Fact] - public void OnProvidersExecuting_UpdatesEntriesWhenActionDescriptorProviderCollectionIsUpdated() + public void OnProvidersExecuting_DoesNotInvokePageLoader_WhenEndpointRoutingIsUsed() + { + // Arrange + var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor + { + RelativePath = "/Path1", + FilterDescriptors = new FilterDescriptor[0], + }); + + var loader = new Mock(); + var invokerProvider = CreateInvokerProvider( + loader.Object, + CreateActionDescriptorCollection(descriptor), + mvcOptions: new MvcOptions { EnableEndpointRouting = true }); + + var context = new ActionInvokerProviderContext(new ActionContext + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); + + // Act + invokerProvider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.Result); + Assert.IsType(context.Result); + loader.Verify(l => l.LoadAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public void OnProvidersExecuting_InvokesPageLoader_WithoutEndpointRouting() { // Arrange var descriptor = new PageActionDescriptor @@ -272,6 +290,41 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure FilterDescriptors = new FilterDescriptor[0], }; + var loader = new Mock(); + loader.Setup(l => l.LoadAsync(descriptor)) + .ReturnsAsync(CreateCompiledPageActionDescriptor(descriptor)); + + var invokerProvider = CreateInvokerProvider( + loader.Object, + CreateActionDescriptorCollection(descriptor), + mvcOptions: new MvcOptions { EnableEndpointRouting = false }); + + var context = new ActionInvokerProviderContext(new ActionContext + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); + + // Act + invokerProvider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.Result); + Assert.IsType(context.Result); + loader.Verify(l => l.LoadAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public void OnProvidersExecuting_UpdatesEntriesWhenActionDescriptorProviderCollectionIsUpdated() + { + // Arrange + var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor + { + RelativePath = "/Path1", + FilterDescriptors = new FilterDescriptor[0], + }); + var descriptorCollection1 = new ActionDescriptorCollection(new[] { descriptor }, version: 1); var descriptorCollection2 = new ActionDescriptorCollection(new[] { descriptor }, version: 2); @@ -281,13 +334,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure .Returns(descriptorCollection1) .Returns(descriptorCollection2); - var loader = new Mock(); - loader - .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor)); + var loader = Mock.Of(); var invokerProvider = CreateInvokerProvider( - loader.Object, + loader, actionDescriptorProvider.Object); var context = new ActionInvokerProviderContext(new ActionContext() @@ -332,10 +382,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure PageTypeInfo = typeof(object).GetTypeInfo(), }; - var loader = new Mock(); + var loader = new Mock(); loader - .Setup(l => l.Load(It.IsAny())) - .Returns(compiledPageDescriptor); + .Setup(l => l.LoadAsync(It.IsAny())) + .ReturnsAsync(compiledPageDescriptor); var mock = new Mock(MockBehavior.Strict); mock @@ -382,10 +432,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure ViewEnginePath = "/Views/Deeper/Index.cshtml" }; - var loader = new Mock(); + var loader = new Mock(); loader - .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel))); + .Setup(l => l.LoadAsync(It.IsAny())) + .ReturnsAsync(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel))); var pageFactory = new Mock(); pageFactory @@ -447,11 +497,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } private static PageActionInvokerProvider CreateInvokerProvider( - IPageLoader loader, + PageLoader loader, IActionDescriptorCollectionProvider actionDescriptorProvider, IPageFactoryProvider pageProvider = null, IPageModelFactoryProvider modelProvider = null, - IRazorPageFactoryProvider razorPageFactoryProvider = null) + IRazorPageFactoryProvider razorPageFactoryProvider = null, + MvcOptions mvcOptions = null) { var tempDataFactory = new Mock(); tempDataFactory @@ -460,7 +511,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var modelBinderFactory = TestModelBinderFactory.CreateDefault(); - var mvcOptions = new MvcOptions(); + mvcOptions = mvcOptions ?? new MvcOptions(); var parameterBinder = new ParameterBinder( modelMetadataProvider, @@ -480,7 +531,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure modelMetadataProvider, modelBinderFactory, tempDataFactory.Object, - Options.Create(new MvcOptions()), + Options.Create(mvcOptions), Options.Create(new HtmlHelperOptions()), Mock.Of(), new DiagnosticListener("Microsoft.AspNetCore"), diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageLoaderMatcherPolicyTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageLoaderMatcherPolicyTest.cs new file mode 100644 index 0000000000..f678690597 --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/PageLoaderMatcherPolicyTest.cs @@ -0,0 +1,88 @@ +// 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 System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class PageLoaderMatcherPolicyTest + { + [Fact] + public async Task ApplyAsync_UpdatesCandidateSet() + { + // Arrange + var compiled = new CompiledPageActionDescriptor(); + compiled.Endpoint = CreateEndpoint(new PageActionDescriptor()); + + var candidateSet = CreateCandidateSet(compiled); + + var loader = Mock.Of(p => p.LoadAsync(It.IsAny()) == Task.FromResult(compiled)); + var policy = new PageLoaderMatcherPolicy(loader); + + // Act + await policy.ApplyAsync(new DefaultHttpContext(), new EndpointSelectorContext(), candidateSet); + + // Assert + Assert.Same(compiled.Endpoint, candidateSet[0].Endpoint); + } + + [Fact] + public async Task ApplyAsync_UpdatesCandidateSet_IfLoaderReturnsAsynchronously() + { + // Arrange + var compiled = new CompiledPageActionDescriptor(); + compiled.Endpoint = CreateEndpoint(new PageActionDescriptor()); + + var tcs = new TaskCompletionSource(); + var candidateSet = CreateCandidateSet(compiled); + + var loadTask = Task.Run(async () => + { + await tcs.Task; + return compiled; + }); + var loader = Mock.Of(p => p.LoadAsync(It.IsAny()) == loadTask); + var policy = new PageLoaderMatcherPolicy(loader); + + // Act + var applyTask = policy.ApplyAsync(new DefaultHttpContext(), new EndpointSelectorContext(), candidateSet); + tcs.SetResult(0); + await applyTask; + + // Assert + Assert.Same(compiled.Endpoint, candidateSet[0].Endpoint); + } + + private static Endpoint CreateEndpoint(ActionDescriptor action) + { + var metadata = new List() { action, }; + return new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection(metadata), + $"test: {action?.DisplayName}"); + } + + private static CandidateSet CreateCandidateSet(params ActionDescriptor[] actions) + { + var values = new RouteValueDictionary[actions.Length]; + for (var i = 0; i < actions.Length; i++) + { + values[i] = new RouteValueDictionary(); + } + + var candidateSet = new CandidateSet( + actions.Select(CreateEndpoint).ToArray(), + values, + new int[actions.Length]); + return candidateSet; + } + } +}