Skip to content

Commit 39e5257

Browse files
authored
Make page load async when using endpoint routing (#7938)
* Make page load async Fixes #8016
1 parent 3652b7a commit 39e5257

11 files changed

+480
-84
lines changed

src/Mvc/Mvc.RazorPages/ref/Microsoft.AspNetCore.Mvc.RazorPages.netcoreapp3.0.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ public partial interface IPageHandlerMethodSelector
516516
{
517517
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.HandlerMethodDescriptor Select(Microsoft.AspNetCore.Mvc.RazorPages.PageContext context);
518518
}
519+
[System.ObsoleteAttribute("This type is obsolete. Use PageLoader instead.")]
519520
public partial interface IPageLoader
520521
{
521522
Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor Load(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor);
@@ -534,6 +535,12 @@ public PageBoundPropertyDescriptor() { }
534535
System.Reflection.PropertyInfo Microsoft.AspNetCore.Mvc.Infrastructure.IPropertyInfoParameterDescriptor.PropertyInfo { get { throw null; } }
535536
public System.Reflection.PropertyInfo Property { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
536537
}
538+
public abstract partial class PageLoader : Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.IPageLoader
539+
{
540+
protected PageLoader() { }
541+
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor> LoadAsync(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor);
542+
Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.IPageLoader.Load(Microsoft.AspNetCore.Mvc.RazorPages.PageActionDescriptor actionDescriptor) { throw null; }
543+
}
537544
[System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
538545
public partial class PageModelAttribute : System.Attribute
539546
{

src/Mvc/Mvc.RazorPages/src/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ internal static void AddServices(IServiceCollection services)
116116
services.TryAddSingleton<IPageActivatorProvider, DefaultPageActivatorProvider>();
117117
services.TryAddSingleton<IPageFactoryProvider, DefaultPageFactoryProvider>();
118118

119-
services.TryAddSingleton<IPageLoader, DefaultPageLoader>();
119+
#pragma warning disable CS0618 // Type or member is obsolete
120+
services.TryAddSingleton<IPageLoader>(s => s.GetRequiredService<PageLoader>());
121+
#pragma warning restore CS0618 // Type or member is obsolete
122+
services.TryAddSingleton<PageLoader, DefaultPageLoader>();
120123
services.TryAddSingleton<IPageHandlerMethodSelector, DefaultPageHandlerMethodSelector>();
121124

122125
// Action executors

src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageLoader.cs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,45 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.Linq;
78
using System.Reflection;
9+
using System.Threading.Tasks;
810
using Microsoft.AspNetCore.Builder;
911
using Microsoft.AspNetCore.Http;
1012
using Microsoft.AspNetCore.Mvc.ApplicationModels;
1113
using Microsoft.AspNetCore.Mvc.Filters;
14+
using Microsoft.AspNetCore.Mvc.Infrastructure;
1215
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
1316
using Microsoft.AspNetCore.Mvc.Routing;
1417
using Microsoft.Extensions.Options;
1518

1619
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
1720
{
18-
internal class DefaultPageLoader : IPageLoader
21+
internal class DefaultPageLoader : PageLoader
1922
{
23+
private readonly IActionDescriptorCollectionProvider _collectionProvider;
2024
private readonly IPageApplicationModelProvider[] _applicationModelProviders;
2125
private readonly IViewCompilerProvider _viewCompilerProvider;
2226
private readonly ActionEndpointFactory _endpointFactory;
2327
private readonly PageConventionCollection _conventions;
2428
private readonly FilterCollection _globalFilters;
29+
private volatile InnerCache _currentCache;
2530

2631
public DefaultPageLoader(
32+
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
2733
IEnumerable<IPageApplicationModelProvider> applicationModelProviders,
2834
IViewCompilerProvider viewCompilerProvider,
2935
ActionEndpointFactory endpointFactory,
3036
IOptions<RazorPagesOptions> pageOptions,
3137
IOptions<MvcOptions> mvcOptions)
3238
{
39+
_collectionProvider = actionDescriptorCollectionProvider;
3340
_applicationModelProviders = applicationModelProviders
3441
.OrderBy(p => p.Order)
3542
.ToArray();
43+
3644
_viewCompilerProvider = viewCompilerProvider;
3745
_endpointFactory = endpointFactory;
3846
_conventions = pageOptions.Value.Conventions;
@@ -41,16 +49,42 @@ public DefaultPageLoader(
4149

4250
private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler();
4351

44-
public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor)
52+
private ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescriptor>> CurrentCache
53+
{
54+
get
55+
{
56+
var current = _currentCache;
57+
var actionDescriptors = _collectionProvider.ActionDescriptors;
58+
59+
if (current == null || current.Version != actionDescriptors.Version)
60+
{
61+
current = new InnerCache(actionDescriptors.Version);
62+
_currentCache = current;
63+
}
64+
65+
return current.Entries;
66+
}
67+
}
68+
69+
public override Task<CompiledPageActionDescriptor> LoadAsync(PageActionDescriptor actionDescriptor)
4570
{
4671
if (actionDescriptor == null)
4772
{
4873
throw new ArgumentNullException(nameof(actionDescriptor));
4974
}
5075

51-
var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath);
52-
var viewDescriptor = compileTask.GetAwaiter().GetResult();
76+
var cache = CurrentCache;
77+
if (cache.TryGetValue(actionDescriptor, out var compiledDescriptorTask))
78+
{
79+
return compiledDescriptorTask;
80+
}
5381

82+
return cache.GetOrAdd(actionDescriptor, LoadAsyncCore(actionDescriptor));
83+
}
84+
85+
private async Task<CompiledPageActionDescriptor> LoadAsyncCore(PageActionDescriptor actionDescriptor)
86+
{
87+
var viewDescriptor = await Compiler.CompileAsync(actionDescriptor.RelativePath);
5488
var context = new PageApplicationModelProviderContext(actionDescriptor, viewDescriptor.Type.GetTypeInfo());
5589
for (var i = 0; i < _applicationModelProviders.Length; i++)
5690
{
@@ -65,7 +99,7 @@ public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor)
6599
ApplyConventions(_conventions, context.PageApplicationModel);
66100

67101
var compiled = CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel, _globalFilters);
68-
102+
69103
// We need to create an endpoint for routing to use and attach it to the CompiledPageActionDescriptor...
70104
// routing for pages is two-phase. First we perform routing using the route info - we can do this without
71105
// compiling/loading the page. Then once we have a match we load the page and we can create an endpoint
@@ -128,5 +162,18 @@ IEnumerable<TConvention> GetConventions<TConvention>(
128162
attributes.OfType<TConvention>());
129163
}
130164
}
165+
166+
private sealed class InnerCache
167+
{
168+
public InnerCache(int version)
169+
{
170+
Version = version;
171+
Entries = new ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescriptor>>();
172+
}
173+
174+
public ConcurrentDictionary<PageActionDescriptor, Task<CompiledPageActionDescriptor>> Entries { get; }
175+
176+
public int Version { get; }
177+
}
131178
}
132179
}

src/Mvc/Mvc.RazorPages/src/Infrastructure/IPageLoader.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
46
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
57
{
68
/// <summary>
79
/// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>.
810
/// </summary>
11+
[Obsolete("This type is obsolete. Use " + nameof(PageLoader) + " instead.")]
912
public interface IPageLoader
1013
{
1114
/// <summary>

src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvokerProvider.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.Linq;
9+
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.Mvc.Abstractions;
1011
using Microsoft.AspNetCore.Mvc.Filters;
1112
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -19,9 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
1920
{
2021
internal class PageActionInvokerProvider : IActionInvokerProvider
2122
{
22-
private const string ViewStartFileName = "_ViewStart.cshtml";
23-
24-
private readonly IPageLoader _loader;
23+
private readonly PageLoader _loader;
2524
private readonly IPageFactoryProvider _pageFactoryProvider;
2625
private readonly IPageModelFactoryProvider _modelFactoryProvider;
2726
private readonly IModelBinderFactory _modelBinderFactory;
@@ -43,7 +42,7 @@ internal class PageActionInvokerProvider : IActionInvokerProvider
4342
private volatile InnerCache _currentCache;
4443

4544
public PageActionInvokerProvider(
46-
IPageLoader loader,
45+
PageLoader loader,
4746
IPageFactoryProvider pageFactoryProvider,
4847
IPageModelFactoryProvider modelFactoryProvider,
4948
IRazorPageFactoryProvider razorPageFactoryProvider,
@@ -81,7 +80,7 @@ public PageActionInvokerProvider(
8180
}
8281

8382
public PageActionInvokerProvider(
84-
IPageLoader loader,
83+
PageLoader loader,
8584
IPageFactoryProvider pageFactoryProvider,
8685
IPageModelFactoryProvider modelFactoryProvider,
8786
IRazorPageFactoryProvider razorPageFactoryProvider,
@@ -139,7 +138,20 @@ public void OnProvidersExecuting(ActionInvokerProviderContext context)
139138
IFilterMetadata[] filters;
140139
if (!cache.Entries.TryGetValue(actionDescriptor, out var cacheEntry))
141140
{
142-
actionContext.ActionDescriptor = _loader.Load(actionDescriptor);
141+
CompiledPageActionDescriptor compiledPageActionDescriptor;
142+
if (_mvcOptions.EnableEndpointRouting)
143+
{
144+
// With endpoint routing, PageLoaderMatcherPolicy should have already produced a CompiledPageActionDescriptor.
145+
compiledPageActionDescriptor = (CompiledPageActionDescriptor)actionDescriptor;
146+
}
147+
else
148+
{
149+
// With legacy routing, we're forced to perform a blocking call. The exceptation is that
150+
// in the most common case - build time views or successsively cached runtime views - this should finish synchronously.
151+
compiledPageActionDescriptor = _loader.LoadAsync(actionDescriptor).GetAwaiter().GetResult();
152+
}
153+
154+
actionContext.ActionDescriptor = compiledPageActionDescriptor;
143155

144156
var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, actionContext);
145157
filters = filterFactoryResult.Filters;
@@ -285,7 +297,7 @@ private static PageHandlerExecutorDelegate[] GetHandlerExecutors(CompiledPageAct
285297

286298
private PageHandlerBinderDelegate[] GetHandlerBinders(CompiledPageActionDescriptor actionDescriptor)
287299
{
288-
if (actionDescriptor.HandlerMethods == null ||actionDescriptor.HandlerMethods.Count == 0)
300+
if (actionDescriptor.HandlerMethods == null || actionDescriptor.HandlerMethods.Count == 0)
289301
{
290302
return Array.Empty<PageHandlerBinderDelegate>();
291303
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
7+
{
8+
/// <summary>
9+
/// Creates a <see cref="CompiledPageActionDescriptor"/> from a <see cref="PageActionDescriptor"/>.
10+
/// </summary>
11+
#pragma warning disable CS0618 // Type or member is obsolete
12+
public abstract class PageLoader : IPageLoader
13+
#pragma warning restore CS0618 // Type or member is obsolete
14+
{
15+
/// <summary>
16+
/// Produces a <see cref="CompiledPageActionDescriptor"/> given a <see cref="PageActionDescriptor"/>.
17+
/// </summary>
18+
/// <param name="actionDescriptor">The <see cref="PageActionDescriptor"/>.</param>
19+
/// <returns>A <see cref="Task"/> that on completion returns a <see cref="CompiledPageActionDescriptor"/>.</returns>
20+
public abstract Task<CompiledPageActionDescriptor> LoadAsync(PageActionDescriptor actionDescriptor);
21+
22+
CompiledPageActionDescriptor IPageLoader.Load(PageActionDescriptor actionDescriptor)
23+
=> LoadAsync(actionDescriptor).GetAwaiter().GetResult();
24+
}
25+
}

src/Mvc/Mvc.RazorPages/src/Infrastructure/PageLoaderMatcherPolicy.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Routing;
99
using Microsoft.AspNetCore.Routing.Matching;
10-
using Microsoft.Extensions.DependencyInjection;
1110

1211
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
1312
{
1413
internal class PageLoaderMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
1514
{
16-
private readonly IPageLoader _loader;
15+
private readonly PageLoader _loader;
1716

18-
public PageLoaderMatcherPolicy(IPageLoader loader)
17+
public PageLoaderMatcherPolicy(PageLoader loader)
1918
{
2019
if (loader == null)
2120
{
@@ -69,21 +68,51 @@ public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context,
6968
{
7069
throw new ArgumentNullException(nameof(candidates));
7170
}
72-
71+
7372
for (var i = 0; i < candidates.Count; i++)
7473
{
7574
ref var candidate = ref candidates[i];
76-
var endpoint = (RouteEndpoint)candidate.Endpoint;
75+
var endpoint = candidate.Endpoint;
7776

7877
var page = endpoint.Metadata.GetMetadata<PageActionDescriptor>();
7978
if (page != null)
8079
{
81-
var compiled = _loader.Load(page);
82-
candidates.ReplaceEndpoint(i, compiled.Endpoint, candidate.Values);
80+
// We found an endpoint instance that has a PageActionDescriptor, but not a
81+
// CompiledPageActionDescriptor. Update the CandidateSet.
82+
var compiled = _loader.LoadAsync(page);
83+
if (compiled.IsCompletedSuccessfully)
84+
{
85+
candidates.ReplaceEndpoint(i, compiled.Result.Endpoint, candidate.Values);
86+
}
87+
else
88+
{
89+
// In the most common case, GetOrAddAsync will return a synchronous result.
90+
// Avoid going async since this is a fairly hot path.
91+
return ApplyAsyncAwaited(candidates, compiled, i);
92+
}
8393
}
8494
}
8595

8696
return Task.CompletedTask;
8797
}
98+
99+
private async Task ApplyAsyncAwaited(CandidateSet candidates, Task<CompiledPageActionDescriptor> actionDescriptorTask, int index)
100+
{
101+
var compiled = await actionDescriptorTask;
102+
candidates.ReplaceEndpoint(index, compiled.Endpoint, candidates[index].Values);
103+
104+
for (var i = index + 1; i < candidates.Count; i++)
105+
{
106+
var candidate = candidates[i];
107+
var endpoint = candidate.Endpoint;
108+
109+
var page = endpoint.Metadata.GetMetadata<PageActionDescriptor>();
110+
if (page != null)
111+
{
112+
compiled = await _loader.LoadAsync(page);
113+
candidates.ReplaceEndpoint(i, compiled.Endpoint, candidates[i].Values);
114+
}
115+
}
116+
}
88117
}
89118
}

src/Mvc/Mvc.RazorPages/src/PageActionDescriptor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,6 @@ public override string DisplayName
8181
}
8282
}
8383

84-
private string DebuggerDisplayString => $"{{ViewEnginePath = {nameof(ViewEnginePath)}, RelativePath = {nameof(RelativePath)}}}";
84+
private string DebuggerDisplayString => $"{nameof(ViewEnginePath)} = {ViewEnginePath}, {nameof(RelativePath)} = {RelativePath}";
8585
}
8686
}

0 commit comments

Comments
 (0)