Skip to content

Make page load async when using endpoint routing #7938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ public partial interface IPageHandlerMethodSelector
{
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);
Expand All @@ -534,6 +535,12 @@ public PageBoundPropertyDescriptor() { }
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<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)]
public partial class PageModelAttribute : System.Attribute
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ internal static void AddServices(IServiceCollection services)
services.TryAddSingleton<IPageActivatorProvider, DefaultPageActivatorProvider>();
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>();

// Action executors
Expand Down
57 changes: 52 additions & 5 deletions src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IPageApplicationModelProvider> applicationModelProviders,
IViewCompilerProvider viewCompilerProvider,
ActionEndpointFactory endpointFactory,
IOptions<RazorPagesOptions> pageOptions,
IOptions<MvcOptions> mvcOptions)
{
_collectionProvider = actionDescriptorCollectionProvider;
_applicationModelProviders = applicationModelProviders
.OrderBy(p => p.Order)
.ToArray();

_viewCompilerProvider = viewCompilerProvider;
_endpointFactory = endpointFactory;
_conventions = pageOptions.Value.Conventions;
Expand All @@ -41,16 +49,42 @@ public DefaultPageLoader(

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)
{
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<CompiledPageActionDescriptor> 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++)
{
Expand All @@ -65,7 +99,7 @@ public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor)
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
Expand Down Expand Up @@ -128,5 +162,18 @@ IEnumerable<TConvention> GetConventions<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; }
}
}
}
3 changes: 3 additions & 0 deletions src/Mvc/Mvc.RazorPages/src/Infrastructure/IPageLoader.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>.
/// </summary>
[Obsolete("This type is obsolete. Use " + nameof(PageLoader) + " instead.")]
public interface IPageLoader
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand All @@ -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;
Expand All @@ -43,7 +42,7 @@ internal class PageActionInvokerProvider : IActionInvokerProvider
private volatile InnerCache _currentCache;

public PageActionInvokerProvider(
IPageLoader loader,
PageLoader loader,
IPageFactoryProvider pageFactoryProvider,
IPageModelFactoryProvider modelFactoryProvider,
IRazorPageFactoryProvider razorPageFactoryProvider,
Expand Down Expand Up @@ -81,7 +80,7 @@ public PageActionInvokerProvider(
}

public PageActionInvokerProvider(
IPageLoader loader,
PageLoader loader,
IPageFactoryProvider pageFactoryProvider,
IPageModelFactoryProvider modelFactoryProvider,
IRazorPageFactoryProvider razorPageFactoryProvider,
Expand Down Expand Up @@ -139,7 +138,20 @@ public void OnProvidersExecuting(ActionInvokerProviderContext context)
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;
Expand Down Expand Up @@ -285,7 +297,7 @@ private static PageHandlerExecutorDelegate[] GetHandlerExecutors(CompiledPageAct

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>();
}
Expand Down
25 changes: 25 additions & 0 deletions src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoader.cs
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOCS YO


CompiledPageActionDescriptor IPageLoader.Load(PageActionDescriptor actionDescriptor)
=> LoadAsync(actionDescriptor).GetAwaiter().GetResult();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
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)
{
Expand Down Expand Up @@ -69,21 +68,51 @@ public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context,
{
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<PageActionDescriptor>();
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<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);
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Mvc/Mvc.RazorPages/src/PageActionDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ public override string DisplayName
}
}

private string DebuggerDisplayString => $"{{ViewEnginePath = {nameof(ViewEnginePath)}, RelativePath = {nameof(RelativePath)}}}";
private string DebuggerDisplayString => $"{nameof(ViewEnginePath)} = {ViewEnginePath}, {nameof(RelativePath)} = {RelativePath}";
}
}
Loading