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);
}
[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<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
{

View File

@ -116,7 +116,10 @@ namespace Microsoft.Extensions.DependencyInjection
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

View File

@ -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;
@ -41,16 +49,42 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
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++)
{
@ -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<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.
// 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>

View File

@ -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<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.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<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);
}
}
}
}
}

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.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<ActionDescriptor>(), 1);
ActionDescriptorCollectionProvider = Mock.Of<IActionDescriptorCollectionProvider>(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<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]
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<IViewCompiler>();
compiler.Setup(c => c.CompileAsync(It.IsAny<string>()))
.ReturnsAsync(descriptor);

View File

@ -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<PageContext, ViewContext, object> factory = (a, b) => null;
Action<PageContext, ViewContext, object> releaser = (a, b, c) => { };
var loader = new Mock<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor));
var loader = Mock.Of<PageLoader>();
var pageFactoryProvider = new Mock<IPageFactoryProvider>();
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<PageContext, ViewContext, object> factory = (a, b) => null;
Action<PageContext, ViewContext, object> releaser = (a, b, c) => { };
Func<PageContext, object> modelFactory = _ => null;
Action<PageContext, object> modelDisposer = (_, __) => { };
var loader = new Mock<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(
descriptor,
pageType: typeof(PageWithModel),
modelType: typeof(DerivedTestPageModel)));
var loader = Mock.Of<PageLoader>();
var pageFactoryProvider = new Mock<IPageFactoryProvider>();
pageFactoryProvider
.Setup(f => f.CreatePageFactory(It.IsAny<CompiledPageActionDescriptor>()))
@ -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<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel)));
}, pageType: typeof(PageWithModel));
var loader = Mock.Of<PageLoader>();
var razorPageFactoryProvider = new Mock<IRazorPageFactoryProvider>();
Func<IRazorPage> 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<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor));
var loader = Mock.Of<PageLoader>();
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<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
var descriptor = new PageActionDescriptor
@ -272,6 +290,41 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
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 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<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor));
var loader = Mock.Of<PageLoader>();
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<IPageLoader>();
var loader = new Mock<PageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(compiledPageDescriptor);
.Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()))
.ReturnsAsync(compiledPageDescriptor);
var mock = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
mock
@ -382,10 +432,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
ViewEnginePath = "/Views/Deeper/Index.cshtml"
};
var loader = new Mock<IPageLoader>();
var loader = new Mock<PageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel)));
.Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>()))
.ReturnsAsync(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel)));
var pageFactory = new Mock<IRazorPageFactoryProvider>();
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<ITempDataDictionaryFactory>();
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<IPageHandlerMethodSelector>(),
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;
}
}
}