Make page load async when using endpoint routing (#7938)

* Make page load async

Fixes #8016
This commit is contained in:
Pranav K 2019-03-04 14:45:59 -08:00 committed by GitHub
parent 3652b7a927
commit 39e52578d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 480 additions and 84 deletions

View File

@ -516,6 +516,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.HandlerMethodDescriptor Select(Microsoft.AspNetCore.Mvc.RazorPages.PageContext context); 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 public partial interface IPageLoader
{ {
Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor Load(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor); 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; } } 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 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<Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor> 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)] [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
public partial class PageModelAttribute : System.Attribute public partial class PageModelAttribute : System.Attribute
{ {

View File

@ -116,7 +116,10 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IPageActivatorProvider, DefaultPageActivatorProvider>(); services.TryAddSingleton<IPageActivatorProvider, DefaultPageActivatorProvider>();
services.TryAddSingleton<IPageFactoryProvider, DefaultPageFactoryProvider>(); services.TryAddSingleton<IPageFactoryProvider, DefaultPageFactoryProvider>();
services.TryAddSingleton<IPageLoader, DefaultPageLoader>(); #pragma warning disable CS0618 // Type or member is obsolete
services.TryAddSingleton<IPageLoader>(s => s.GetRequiredService<PageLoader>());
#pragma warning restore CS0618 // Type or member is obsolete
services.TryAddSingleton<PageLoader, DefaultPageLoader>();
services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>(); services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>();
// Action executors // Action executors

View File

@ -2,37 +2,45 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
internal class DefaultPageLoader : IPageLoader internal class DefaultPageLoader : PageLoader
{ {
private readonly IActionDescriptorCollectionProvider _collectionProvider;
private readonly IPageApplicationModelProvider[] _applicationModelProviders; private readonly IPageApplicationModelProvider[] _applicationModelProviders;
private readonly IViewCompilerProvider _viewCompilerProvider; private readonly IViewCompilerProvider _viewCompilerProvider;
private readonly ActionEndpointFactory _endpointFactory; private readonly ActionEndpointFactory _endpointFactory;
private readonly PageConventionCollection _conventions; private readonly PageConventionCollection _conventions;
private readonly FilterCollection _globalFilters; private readonly FilterCollection _globalFilters;
private volatile InnerCache _currentCache;
public DefaultPageLoader( public DefaultPageLoader(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
IEnumerable<IPageApplicationModelProvider> applicationModelProviders, IEnumerable<IPageApplicationModelProvider> applicationModelProviders,
IViewCompilerProvider viewCompilerProvider, IViewCompilerProvider viewCompilerProvider,
ActionEndpointFactory endpointFactory, ActionEndpointFactory endpointFactory,
IOptions<RazorPagesOptions> pageOptions, IOptions<RazorPagesOptions> pageOptions,
IOptions<MvcOptions> mvcOptions) IOptions<MvcOptions> mvcOptions)
{ {
_collectionProvider = actionDescriptorCollectionProvider;
_applicationModelProviders = applicationModelProviders _applicationModelProviders = applicationModelProviders
.OrderBy(p => p.Order) .OrderBy(p => p.Order)
.ToArray(); .ToArray();
_viewCompilerProvider = viewCompilerProvider; _viewCompilerProvider = viewCompilerProvider;
_endpointFactory = endpointFactory; _endpointFactory = endpointFactory;
_conventions = pageOptions.Value.Conventions; _conventions = pageOptions.Value.Conventions;
@ -41,16 +49,42 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler();
public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor) private ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescriptor>> 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<CompiledPageActionDescriptor> LoadAsync(PageActionDescriptor actionDescriptor)
{ {
if (actionDescriptor == null) if (actionDescriptor == null)
{ {
throw new ArgumentNullException(nameof(actionDescriptor)); throw new ArgumentNullException(nameof(actionDescriptor));
} }
var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); var cache = CurrentCache;
var viewDescriptor = compileTask.GetAwaiter().GetResult(); if (cache.TryGetValue(actionDescriptor, out var compiledDescriptorTask))
{
return compiledDescriptorTask;
}
return cache.GetOrAdd(actionDescriptor, LoadAsyncCore(actionDescriptor));
}
private async Task<CompiledPageActionDescriptor> LoadAsyncCore(PageActionDescriptor actionDescriptor)
{
var viewDescriptor = await Compiler.CompileAsync(actionDescriptor.RelativePath);
var context = new PageApplicationModelProviderContext(actionDescriptor, viewDescriptor.Type.GetTypeInfo()); var context = new PageApplicationModelProviderContext(actionDescriptor, viewDescriptor.Type.GetTypeInfo());
for (var i = 0; i < _applicationModelProviders.Length; i++) for (var i = 0; i < _applicationModelProviders.Length; i++)
{ {
@ -65,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
ApplyConventions(_conventions, context.PageApplicationModel); ApplyConventions(_conventions, context.PageApplicationModel);
var compiled = CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel, _globalFilters); var compiled = CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel, _globalFilters);
// We need to create an endpoint for routing to use and attach it to the CompiledPageActionDescriptor... // 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 // 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 // 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<TConvention>()); attributes.OfType<TConvention>());
} }
} }
private sealed class InnerCache
{
public InnerCache(int version)
{
Version = version;
Entries = new ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescriptor>>();
}
public ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescriptor>> Entries { get; }
public int Version { get; }
}
} }
} }

View File

@ -1,11 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
/// <summary> /// <summary>
/// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>. /// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>.
/// </summary> /// </summary>
[Obsolete("This type is obsolete. Use " + nameof(PageLoader) + " instead.")]
public interface IPageLoader public interface IPageLoader
{ {
/// <summary> /// <summary>

View File

@ -6,6 +6,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
@ -19,9 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
internal class PageActionInvokerProvider : IActionInvokerProvider internal class PageActionInvokerProvider : IActionInvokerProvider
{ {
private const string ViewStartFileName = "_ViewStart.cshtml"; private readonly PageLoader _loader;
private readonly IPageLoader _loader;
private readonly IPageFactoryProvider _pageFactoryProvider; private readonly IPageFactoryProvider _pageFactoryProvider;
private readonly IPageModelFactoryProvider _modelFactoryProvider; private readonly IPageModelFactoryProvider _modelFactoryProvider;
private readonly IModelBinderFactory _modelBinderFactory; private readonly IModelBinderFactory _modelBinderFactory;
@ -43,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private volatile InnerCache _currentCache; private volatile InnerCache _currentCache;
public PageActionInvokerProvider( public PageActionInvokerProvider(
IPageLoader loader, PageLoader loader,
IPageFactoryProvider pageFactoryProvider, IPageFactoryProvider pageFactoryProvider,
IPageModelFactoryProvider modelFactoryProvider, IPageModelFactoryProvider modelFactoryProvider,
IRazorPageFactoryProvider razorPageFactoryProvider, IRazorPageFactoryProvider razorPageFactoryProvider,
@ -81,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
} }
public PageActionInvokerProvider( public PageActionInvokerProvider(
IPageLoader loader, PageLoader loader,
IPageFactoryProvider pageFactoryProvider, IPageFactoryProvider pageFactoryProvider,
IPageModelFactoryProvider modelFactoryProvider, IPageModelFactoryProvider modelFactoryProvider,
IRazorPageFactoryProvider razorPageFactoryProvider, IRazorPageFactoryProvider razorPageFactoryProvider,
@ -139,7 +138,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
IFilterMetadata[] filters; IFilterMetadata[] filters;
if (!cache.Entries.TryGetValue(actionDescriptor, out var cacheEntry)) 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); var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, actionContext);
filters = filterFactoryResult.Filters; filters = filterFactoryResult.Filters;
@ -285,7 +297,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private PageHandlerBinderDelegate[] GetHandlerBinders(CompiledPageActionDescriptor actionDescriptor) private PageHandlerBinderDelegate[] GetHandlerBinders(CompiledPageActionDescriptor actionDescriptor)
{ {
if (actionDescriptor.HandlerMethods == null ||actionDescriptor.HandlerMethods.Count == 0) if (actionDescriptor.HandlerMethods == null || actionDescriptor.HandlerMethods.Count == 0)
{ {
return Array.Empty<PageHandlerBinderDelegate>(); return Array.Empty<PageHandlerBinderDelegate>();
} }

View File

@ -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
{
/// <summary>
/// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
public abstract class PageLoader : IPageLoader
#pragma warning restore CS0618 // Type or member is obsolete
{
/// <summary>
/// Produces a <see cref="CompiledPageActionDescriptor"/> given a <see cref="PageActionDescriptor"/>.
/// </summary>
/// <param name="actionDescriptor">The <see cref="PageActionDescriptor"/>.</param>
/// <returns>A <see cref="Task"/> that on completion returns a <see cref="CompiledPageActionDescriptor"/>.</returns>
public abstract Task<CompiledPageActionDescriptor> LoadAsync(PageActionDescriptor actionDescriptor);
CompiledPageActionDescriptor IPageLoader.Load(PageActionDescriptor actionDescriptor)
=> LoadAsync(actionDescriptor).GetAwaiter().GetResult();
}
}

View File

@ -7,15 +7,14 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
internal class PageLoaderMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy internal class PageLoaderMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{ {
private readonly IPageLoader _loader; private readonly PageLoader _loader;
public PageLoaderMatcherPolicy(IPageLoader loader) public PageLoaderMatcherPolicy(PageLoader loader)
{ {
if (loader == null) if (loader == null)
{ {
@ -69,21 +68,51 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
throw new ArgumentNullException(nameof(candidates)); throw new ArgumentNullException(nameof(candidates));
} }
for (var i = 0; i < candidates.Count; i++) for (var i = 0; i < candidates.Count; i++)
{ {
ref var candidate = ref candidates[i]; ref var candidate = ref candidates[i];
var endpoint = (RouteEndpoint)candidate.Endpoint; var endpoint = candidate.Endpoint;
var page = endpoint.Metadata.GetMetadata<PageActionDescriptor>(); var page = endpoint.Metadata.GetMetadata<PageActionDescriptor>();
if (page != null) if (page != null)
{ {
var compiled = _loader.Load(page); // We found an endpoint instance that has a PageActionDescriptor, but not a
candidates.ReplaceEndpoint(i, compiled.Endpoint, candidate.Values); // 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; return Task.CompletedTask;
} }
private async Task ApplyAsyncAwaited(CandidateSet candidates, Task<CompiledPageActionDescriptor> 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<PageActionDescriptor>();
if (page != null)
{
compiled = await _loader.LoadAsync(page);
candidates.ReplaceEndpoint(i, compiled.Endpoint, candidates[i].Values);
}
}
}
} }
} }

View File

@ -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}";
} }
} }

View File

@ -3,6 +3,8 @@
using System; using System;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Compilation;
@ -18,8 +20,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
public class DefaultPageLoaderTest public class DefaultPageLoaderTest
{ {
private readonly IActionDescriptorCollectionProvider ActionDescriptorCollectionProvider;
public DefaultPageLoaderTest()
{
var actionDescriptors = new ActionDescriptorCollection(Array.Empty<ActionDescriptor>(), 1);
ActionDescriptorCollectionProvider = Mock.Of<IActionDescriptorCollectionProvider>(v => v.ActionDescriptors == actionDescriptors);
}
[Fact] [Fact]
public void Load_InvokesApplicationModelProviders() public async Task LoadAsync_InvokesApplicationModelProviders()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor(); var descriptor = new PageActionDescriptor();
@ -77,6 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
}; };
var loader = new DefaultPageLoader( var loader = new DefaultPageLoader(
ActionDescriptorCollectionProvider,
providers, providers,
compilerProvider, compilerProvider,
endpointFactory, endpointFactory,
@ -84,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
mvcOptions); mvcOptions);
// Act // Act
var result = loader.Load(new PageActionDescriptor()); var result = await loader.LoadAsync(new PageActionDescriptor());
// Assert // Assert
provider1.Verify(); provider1.Verify();
@ -92,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
} }
[Fact] [Fact]
public void Load_CreatesEndpoint_WithRoute() public async Task LoadAsync_CreatesEndpoint_WithRoute()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor() var descriptor = new PageActionDescriptor()
@ -132,6 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
}; };
var loader = new DefaultPageLoader( var loader = new DefaultPageLoader(
ActionDescriptorCollectionProvider,
providers, providers,
compilerProvider, compilerProvider,
endpointFactory, endpointFactory,
@ -139,14 +151,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
mvcOptions); mvcOptions);
// Act // Act
var result = loader.Load(descriptor); var result = await loader.LoadAsync(descriptor);
// Assert // Assert
Assert.NotNull(result.Endpoint); Assert.NotNull(result.Endpoint);
} }
[Fact] [Fact]
public void Load_InvokesApplicationModelProviders_WithTheRightOrder() public async Task LoadAsync_InvokesApplicationModelProviders_WithTheRightOrder()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor(); var descriptor = new PageActionDescriptor();
@ -196,6 +208,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
}; };
var loader = new DefaultPageLoader( var loader = new DefaultPageLoader(
ActionDescriptorCollectionProvider,
providers, providers,
compilerProvider, compilerProvider,
endpointFactory, endpointFactory,
@ -203,13 +216,134 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
mvcOptions); mvcOptions);
// Act // Act
var result = loader.Load(new PageActionDescriptor()); var result = await loader.LoadAsync(new PageActionDescriptor());
// Assert // Assert
provider1.Verify(); provider1.Verify();
provider2.Verify(); provider2.Verify();
} }
[Fact]
public async Task LoadAsync_CachesResults()
{
// Arrange
var descriptor = new PageActionDescriptor()
{
AttributeRouteInfo = new AttributeRouteInfo()
{
Template = "/test",
},
};
var transformer = new Mock<RoutePatternTransformer>();
transformer
.Setup(t => t.SubstituteRequiredValues(It.IsAny<RoutePattern>(), It.IsAny<object>()))
.Returns<RoutePattern, object>((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<IPageApplicationModelProvider>();
var pageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty<object>());
provider.Setup(p => p.OnProvidersExecuting(It.IsAny<PageApplicationModelProviderContext>()))
.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<RoutePatternTransformer>();
transformer
.Setup(t => t.SubstituteRequiredValues(It.IsAny<RoutePattern>(), It.IsAny<object>()))
.Returns<RoutePattern, object>((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<IPageApplicationModelProvider>();
var pageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty<object>());
provider.Setup(p => p.OnProvidersExecuting(It.IsAny<PageApplicationModelProviderContext>()))
.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<IActionDescriptorCollectionProvider>();
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] [Fact]
public void ApplyConventions_InvokesApplicationModelConventions() public void ApplyConventions_InvokesApplicationModelConventions()
{ {
@ -553,11 +687,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private static IViewCompilerProvider GetCompilerProvider() private static IViewCompilerProvider GetCompilerProvider()
{ {
var descriptor = new CompiledViewDescriptor var compiledItem = TestRazorCompiledItem.CreateForView(typeof(object), "/Views/Index.cshtml");
{ var descriptor = new CompiledViewDescriptor(compiledItem);
Item = TestRazorCompiledItem.CreateForView(typeof(object), "/Views/Index.cshtml"),
};
var compiler = new Mock<IViewCompiler>(); var compiler = new Mock<IViewCompiler>();
compiler.Setup(c => c.CompileAsync(It.IsAny<string>())) compiler.Setup(c => c.CompileAsync(It.IsAny<string>()))
.ReturnsAsync(descriptor); .ReturnsAsync(descriptor);

View File

@ -30,19 +30,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public void OnProvidersExecuting_WithEmptyModel_PopulatesCacheEntry() public void OnProvidersExecuting_WithEmptyModel_PopulatesCacheEntry()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
{ {
RelativePath = "/Path1", RelativePath = "/Path1",
FilterDescriptors = new FilterDescriptor[0], FilterDescriptors = new FilterDescriptor[0],
}; });
Func<PageContext, ViewContext, object> factory = (a, b) => null; Func<PageContext, ViewContext, object> factory = (a, b) => null;
Action<PageContext, ViewContext, object> releaser = (a, b, c) => { }; Action<PageContext, ViewContext, object> releaser = (a, b, c) => { };
var loader = new Mock<IPageLoader>(); var loader = Mock.Of<PageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor));
var pageFactoryProvider = new Mock<IPageFactoryProvider>(); var pageFactoryProvider = new Mock<IPageFactoryProvider>();
pageFactoryProvider pageFactoryProvider
@ -53,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
.Returns(releaser); .Returns(releaser);
var invokerProvider = CreateInvokerProvider( var invokerProvider = CreateInvokerProvider(
loader.Object, loader,
CreateActionDescriptorCollection(descriptor), CreateActionDescriptorCollection(descriptor),
pageFactoryProvider.Object); pageFactoryProvider.Object);
@ -83,25 +80,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public void OnProvidersExecuting_WithModel_PopulatesCacheEntry() public void OnProvidersExecuting_WithModel_PopulatesCacheEntry()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor var descriptor = CreateCompiledPageActionDescriptor(
{ new PageActionDescriptor
RelativePath = "/Path1", {
FilterDescriptors = new FilterDescriptor[0] RelativePath = "/Path1",
}; FilterDescriptors = new FilterDescriptor[0]
},
pageType: typeof(PageWithModel),
modelType: typeof(DerivedTestPageModel));
Func<PageContext, ViewContext, object> factory = (a, b) => null; Func<PageContext, ViewContext, object> factory = (a, b) => null;
Action<PageContext, ViewContext, object> releaser = (a, b, c) => { }; Action<PageContext, ViewContext, object> releaser = (a, b, c) => { };
Func<PageContext, object> modelFactory = _ => null; Func<PageContext, object> modelFactory = _ => null;
Action<PageContext, object> modelDisposer = (_, __) => { }; Action<PageContext, object> modelDisposer = (_, __) => { };
var loader = new Mock<IPageLoader>(); var loader = Mock.Of<PageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(
descriptor,
pageType: typeof(PageWithModel),
modelType: typeof(DerivedTestPageModel)));
var pageFactoryProvider = new Mock<IPageFactoryProvider>(); var pageFactoryProvider = new Mock<IPageFactoryProvider>();
pageFactoryProvider pageFactoryProvider
.Setup(f => f.CreatePageFactory(It.IsAny<CompiledPageActionDescriptor>())) .Setup(f => f.CreatePageFactory(It.IsAny<CompiledPageActionDescriptor>()))
@ -119,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
.Returns(modelDisposer); .Returns(modelDisposer);
var invokerProvider = CreateInvokerProvider( var invokerProvider = CreateInvokerProvider(
loader.Object, loader,
CreateActionDescriptorCollection(descriptor), CreateActionDescriptorCollection(descriptor),
pageFactoryProvider.Object, pageFactoryProvider.Object,
modelFactoryProvider.Object); modelFactoryProvider.Object);
@ -162,18 +155,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public void OnProvidersExecuting_CachesViewStartFactories() public void OnProvidersExecuting_CachesViewStartFactories()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
{ {
RelativePath = "/Home/Path1/File.cshtml", RelativePath = "/Home/Path1/File.cshtml",
ViewEnginePath = "/Home/Path1/File.cshtml", ViewEnginePath = "/Home/Path1/File.cshtml",
FilterDescriptors = new FilterDescriptor[0], FilterDescriptors = new FilterDescriptor[0],
}; }, pageType: typeof(PageWithModel));
var loader = new Mock<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel)));
var loader = Mock.Of<PageLoader>();
var razorPageFactoryProvider = new Mock<IRazorPageFactoryProvider>(); var razorPageFactoryProvider = new Mock<IRazorPageFactoryProvider>();
Func<IRazorPage> factory1 = () => null; Func<IRazorPage> factory1 = () => null;
@ -191,7 +180,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
fileProvider.AddFile("/_ViewStart.cshtml", "content2"); fileProvider.AddFile("/_ViewStart.cshtml", "content2");
var invokerProvider = CreateInvokerProvider( var invokerProvider = CreateInvokerProvider(
loader.Object, loader,
CreateActionDescriptorCollection(descriptor), CreateActionDescriptorCollection(descriptor),
razorPageFactoryProvider: razorPageFactoryProvider.Object); razorPageFactoryProvider: razorPageFactoryProvider.Object);
@ -216,19 +205,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public void OnProvidersExecuting_CachesEntries() public void OnProvidersExecuting_CachesEntries()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor var descriptor = CreateCompiledPageActionDescriptor(new PageActionDescriptor
{ {
RelativePath = "/Path1", RelativePath = "/Path1",
FilterDescriptors = new FilterDescriptor[0], FilterDescriptors = new FilterDescriptor[0],
}; });
var loader = new Mock<IPageLoader>(); var loader = Mock.Of<PageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor));
var invokerProvider = CreateInvokerProvider( var invokerProvider = CreateInvokerProvider(
loader.Object, loader,
CreateActionDescriptorCollection(descriptor)); CreateActionDescriptorCollection(descriptor));
var context = new ActionInvokerProviderContext(new ActionContext var context = new ActionInvokerProviderContext(new ActionContext
@ -263,7 +249,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
} }
[Fact] [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<PageLoader>();
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<PageActionInvoker>(context.Result);
loader.Verify(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()), Times.Never());
}
[Fact]
public void OnProvidersExecuting_InvokesPageLoader_WithoutEndpointRouting()
{ {
// Arrange // Arrange
var descriptor = new PageActionDescriptor var descriptor = new PageActionDescriptor
@ -272,6 +290,41 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
FilterDescriptors = new FilterDescriptor[0], FilterDescriptors = new FilterDescriptor[0],
}; };
var loader = new Mock<PageLoader>();
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<PageActionInvoker>(context.Result);
loader.Verify(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()), 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 descriptorCollection1 = new ActionDescriptorCollection(new[] { descriptor }, version: 1);
var descriptorCollection2 = new ActionDescriptorCollection(new[] { descriptor }, version: 2); var descriptorCollection2 = new ActionDescriptorCollection(new[] { descriptor }, version: 2);
@ -281,13 +334,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
.Returns(descriptorCollection1) .Returns(descriptorCollection1)
.Returns(descriptorCollection2); .Returns(descriptorCollection2);
var loader = new Mock<IPageLoader>(); var loader = Mock.Of<PageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor));
var invokerProvider = CreateInvokerProvider( var invokerProvider = CreateInvokerProvider(
loader.Object, loader,
actionDescriptorProvider.Object); actionDescriptorProvider.Object);
var context = new ActionInvokerProviderContext(new ActionContext() var context = new ActionInvokerProviderContext(new ActionContext()
@ -332,10 +382,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
PageTypeInfo = typeof(object).GetTypeInfo(), PageTypeInfo = typeof(object).GetTypeInfo(),
}; };
var loader = new Mock<IPageLoader>(); var loader = new Mock<PageLoader>();
loader loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>())) .Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()))
.Returns(compiledPageDescriptor); .ReturnsAsync(compiledPageDescriptor);
var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict); var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
mock mock
@ -382,10 +432,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
ViewEnginePath = "/Views/Deeper/Index.cshtml" ViewEnginePath = "/Views/Deeper/Index.cshtml"
}; };
var loader = new Mock<IPageLoader>(); var loader = new Mock<PageLoader>();
loader loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>())) .Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel))); .ReturnsAsync(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel)));
var pageFactory = new Mock<IRazorPageFactoryProvider>(); var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory pageFactory
@ -447,11 +497,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
} }
private static PageActionInvokerProvider CreateInvokerProvider( private static PageActionInvokerProvider CreateInvokerProvider(
IPageLoader loader, PageLoader loader,
IActionDescriptorCollectionProvider actionDescriptorProvider, IActionDescriptorCollectionProvider actionDescriptorProvider,
IPageFactoryProvider pageProvider = null, IPageFactoryProvider pageProvider = null,
IPageModelFactoryProvider modelProvider = null, IPageModelFactoryProvider modelProvider = null,
IRazorPageFactoryProvider razorPageFactoryProvider = null) IRazorPageFactoryProvider razorPageFactoryProvider = null,
MvcOptions mvcOptions = null)
{ {
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(); var tempDataFactory = new Mock<ITempDataDictionaryFactory>();
tempDataFactory tempDataFactory
@ -460,7 +511,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault(); var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var mvcOptions = new MvcOptions(); mvcOptions = mvcOptions ?? new MvcOptions();
var parameterBinder = new ParameterBinder( var parameterBinder = new ParameterBinder(
modelMetadataProvider, modelMetadataProvider,
@ -480,7 +531,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
modelMetadataProvider, modelMetadataProvider,
modelBinderFactory, modelBinderFactory,
tempDataFactory.Object, tempDataFactory.Object,
Options.Create(new MvcOptions()), Options.Create(mvcOptions),
Options.Create(new HtmlHelperOptions()), Options.Create(new HtmlHelperOptions()),
Mock.Of<IPageHandlerMethodSelector>(), Mock.Of<IPageHandlerMethodSelector>(),
new DiagnosticListener("Microsoft.AspNetCore"), new DiagnosticListener("Microsoft.AspNetCore"),

View File

@ -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<PageLoader>(p => p.LoadAsync(It.IsAny<PageActionDescriptor>()) == 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<int>();
var candidateSet = CreateCandidateSet(compiled);
var loadTask = Task.Run(async () =>
{
await tcs.Task;
return compiled;
});
var loader = Mock.Of<PageLoader>(p => p.LoadAsync(It.IsAny<PageActionDescriptor>()) == 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<object>() { 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;
}
}
}