Introduce dynamic endpoints and fix #7011 (#7445)

* Add IDynamicEndpointMetadata for dynamic endpoints

* Use a dynamic endpoint policy for pages
This commit is contained in:
Ryan Nowak 2019-02-13 18:52:07 -08:00 committed by GitHub
parent 27e54a1b7a
commit f2a1a4542e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 589 additions and 221 deletions

View File

@ -0,0 +1,34 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// A metadata interface that can be used to specify that the associated <see cref="Endpoint" />
/// will be dynamically replaced during matching.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="IDynamicEndpointMetadata"/> and related derived interfaces signal to
/// <see cref="MatcherPolicy"/> implementations that an <see cref="Endpoint"/> has dynamic behavior
/// and thus cannot have its characteristics cached.
/// </para>
/// <para>
/// Using dynamic endpoints can be useful because the default matcher implementation does not
/// supply extensibility for how URLs are processed. Routing implementations that have dynamic
/// behavior can apply their dynamic logic after URL processing, by replacing a endpoints as
/// part of a <see cref="CandidateSet"/>.
/// </para>
/// </remarks>
public interface IDynamicEndpointMetadata
{
/// <summary>
/// Returns a value that indicates whether the associated endpoint has dynamic matching
/// behavior.
/// </summary>
bool IsDynamic { get; }
}
}

View File

@ -1,4 +1,4 @@
// 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.
using System;
@ -281,6 +281,57 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
/// <summary>
/// Replaces the <see cref="Endpoint"/> at the provided <paramref name="index"/> with the
/// provided <paramref name="endpoint"/>.
/// </summary>
/// <param name="index">The candidate index.</param>
/// <param name="endpoint">
/// The <see cref="Endpoint"/> to replace the original <see cref="Endpoint"/> at
/// the <paramref name="index"/>. If <paramref name="endpoint"/> the candidate will be marked
/// as invalid.
/// </param>
/// <param name="values">
/// The <see cref="RouteValueDictionary"/> to replace the original <see cref="RouteValueDictionary"/> at
/// the <paramref name="index"/>.
/// </param>
public void ReplaceEndpoint(int index, Endpoint endpoint, RouteValueDictionary values)
{
// Friendliness for inlining
if ((uint)index >= Count)
{
ThrowIndexArgumentOutOfRangeException();
}
switch (index)
{
case 0:
_state0 = new CandidateState(endpoint, values, _state0.Score);
break;
case 1:
_state1 = new CandidateState(endpoint, values, _state1.Score);
break;
case 2:
_state2 = new CandidateState(endpoint, values, _state2.Score);
break;
case 3:
_state3 = new CandidateState(endpoint, values, _state3.Score);
break;
default:
_additionalCandidates[index - 4] = new CandidateState(endpoint, values, _additionalCandidates[index - 4].Score);
break;
}
if (endpoint == null)
{
SetValidity(index, false);
}
}
private static void ThrowIndexArgumentOutOfRangeException()
{
throw new ArgumentOutOfRangeException("index");

View File

@ -1,6 +1,9 @@
// 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.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
namespace Microsoft.AspNetCore.Routing
@ -24,5 +27,30 @@ namespace Microsoft.AspNetCore.Routing
/// property.
/// </summary>
public abstract int Order { get; }
/// <summary>
/// Returns a value that indicates whether the provided <paramref name="endpoints"/> contains
/// one or more dynamic endpoints.
/// </summary>
/// <param name="endpoints">The set of endpoints.</param>
/// <returns><c>true</c> if a dynamic endpoint is found; otherwise returns <c>false</c>.</returns>
protected static bool ContainsDynamicEndpoints(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
for (var i = 0; i < endpoints.Count; i++)
{
var metadata = endpoints[i].Metadata.GetMetadata<IDynamicEndpointMetadata>();
if (metadata?.IsDynamic == true)
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,92 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing
{
public class MatcherPolicyTest
{
[Fact]
public void ContainsDynamicEndpoint_FindsDynamicEndpoint()
{
// Arrange
var endpoints = new Endpoint[]
{
CreateEndpoint("1"),
CreateEndpoint("2"),
CreateEndpoint("3", new DynamicEndpointMetadata(isDynamic: true)),
};
// Act
var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint()
{
// Arrange
var endpoints = new Endpoint[]
{
CreateEndpoint("1"),
CreateEndpoint("2"),
CreateEndpoint("3", new DynamicEndpointMetadata(isDynamic: false)),
};
// Act
var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints);
// Assert
Assert.False(result);
}
[Fact]
public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint_Empty()
{
// Arrange
var endpoints = new Endpoint[]{ };
// Act
var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints);
// Assert
Assert.False(result);
}
private RouteEndpoint CreateEndpoint(string template, params object[] metadata)
{
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template),
0,
new EndpointMetadataCollection(metadata),
"test");
}
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
{
public DynamicEndpointMetadata(bool isDynamic)
{
IsDynamic = isDynamic;
}
public bool IsDynamic { get; }
}
private class TestMatcherPolicy : MatcherPolicy
{
public override int Order => throw new System.NotImplementedException();
public new static bool ContainsDynamicEndpoints(IReadOnlyList<Endpoint> endpoints)
{
return MatcherPolicy.ContainsDynamicEndpoints(endpoints);
}
}
}
}

View File

@ -1,4 +1,4 @@
// 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.
using System;
@ -55,6 +55,91 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
// of input sizes.
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)]
[InlineData(31)]
[InlineData(32)] // this is the break point where we use a BitArray
[InlineData(33)]
public void ReplaceEndpoint_WithEndpoint(int count)
{
// Arrange
var endpoints = new RouteEndpoint[count];
for (var i = 0; i < endpoints.Length; i++)
{
endpoints[i] = CreateEndpoint($"/{i}");
}
var builder = CreateDfaMatcherBuilder();
var candidates = builder.CreateCandidates(endpoints);
var candidateSet = new CandidateSet(candidates);
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
var endpoint = CreateEndpoint($"/test{i}");
var values = new RouteValueDictionary();
// Act
candidateSet.ReplaceEndpoint(i, endpoint, values);
// Assert
Assert.Same(endpoint, state.Endpoint);
Assert.Same(values, state.Values);
Assert.True(candidateSet.IsValidCandidate(i));
}
}
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
// of input sizes.
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)] // this is the break-point where we start to use a list.
[InlineData(6)]
[InlineData(31)]
[InlineData(32)] // this is the break point where we use a BitArray
[InlineData(33)]
public void ReplaceEndpoint_WithEndpoint_Null(int count)
{
// Arrange
var endpoints = new RouteEndpoint[count];
for (var i = 0; i < endpoints.Length; i++)
{
endpoints[i] = CreateEndpoint($"/{i}");
}
var builder = CreateDfaMatcherBuilder();
var candidates = builder.CreateCandidates(endpoints);
var candidateSet = new CandidateSet(candidates);
for (var i = 0; i < candidateSet.Count; i++)
{
ref var state = ref candidateSet[i];
// Act
candidateSet.ReplaceEndpoint(i, null, null);
// Assert
Assert.Null(state.Endpoint);
Assert.Null(state.Values);
Assert.False(candidateSet.IsValidCandidate(i));
}
}
// We special case low numbers of candidates, so we want to verify that it works correctly for a variety
// of input sizes.
[Theory]

View File

@ -114,10 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Performance
{
var dataSource = new ActionEndpointDataSource(
actionDescriptorCollectionProvider,
new ActionEndpointFactory(
new MockRoutePatternTransformer(),
new MvcEndpointInvokerFactory(
new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>()))));
new ActionEndpointFactory(new MockRoutePatternTransformer()));
return dataSource;
}

View File

@ -272,7 +272,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<ActionEndpointDataSource>();
services.TryAddSingleton<ControllerActionEndpointDataSource>();
services.TryAddSingleton<ActionEndpointFactory>();
services.TryAddSingleton<MvcEndpointInvokerFactory>();
//
// Middleware pipeline filter related

View File

@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public class ActionContextAccessor : IActionContextAccessor
{
internal static readonly IActionContextAccessor Null = new NullActionContextAccessor();
private static readonly AsyncLocal<ActionContext> _storage = new AsyncLocal<ActionContext>();
public ActionContext ActionContext
@ -14,5 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
get { return _storage.Value; }
set { _storage.Value = value; }
}
private class NullActionContextAccessor : IActionContextAccessor
{
public ActionContext ActionContext
{
get => null;
set { }
}
}
}
}

View File

@ -27,11 +27,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
internal ControllerActionInvoker(
ILogger logger,
DiagnosticListener diagnosticListener,
IActionContextAccessor actionContextAccessor,
IActionResultTypeMapper mapper,
ControllerContext controllerContext,
ControllerActionInvokerCacheEntry cacheEntry,
IFilterMetadata[] filters)
: base(diagnosticListener, logger, mapper, controllerContext, filters, controllerContext.ValueProviderFactories)
: base(diagnosticListener, logger, actionContextAccessor, mapper, controllerContext, filters, controllerContext.ValueProviderFactories)
{
if (cacheEntry == null)
{

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
private readonly ILogger _logger;
private readonly DiagnosticListener _diagnosticListener;
private readonly IActionResultTypeMapper _mapper;
private readonly IActionContextAccessor _actionContextAccessor;
public ControllerActionInvokerProvider(
ControllerActionInvokerCache controllerActionInvokerCache,
@ -28,6 +29,17 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
ILoggerFactory loggerFactory,
DiagnosticListener diagnosticListener,
IActionResultTypeMapper mapper)
: this(controllerActionInvokerCache, optionsAccessor, loggerFactory, diagnosticListener, mapper, null)
{
}
public ControllerActionInvokerProvider(
ControllerActionInvokerCache controllerActionInvokerCache,
IOptions<MvcOptions> optionsAccessor,
ILoggerFactory loggerFactory,
DiagnosticListener diagnosticListener,
IActionResultTypeMapper mapper,
IActionContextAccessor actionContextAccessor)
{
_controllerActionInvokerCache = controllerActionInvokerCache;
_valueProviderFactories = optionsAccessor.Value.ValueProviderFactories.ToArray();
@ -35,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
_logger = loggerFactory.CreateLogger<ControllerActionInvoker>();
_diagnosticListener = diagnosticListener;
_mapper = mapper;
_actionContextAccessor = actionContextAccessor ?? ActionContextAccessor.Null;
}
public int Order => -1000;
@ -61,6 +74,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var invoker = new ControllerActionInvoker(
_logger,
_diagnosticListener,
_actionContextAccessor,
_mapper,
controllerContext,
cacheEntry,

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
protected readonly DiagnosticListener _diagnosticListener;
protected readonly ILogger _logger;
protected readonly IActionContextAccessor _actionContextAccessor;
protected readonly IActionResultTypeMapper _mapper;
protected readonly ActionContext _actionContext;
protected readonly IFilterMetadata[] _filters;
@ -39,6 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public ResourceInvoker(
DiagnosticListener diagnosticListener,
ILogger logger,
IActionContextAccessor actionContextAccessor,
IActionResultTypeMapper mapper,
ActionContext actionContext,
IFilterMetadata[] filters,
@ -46,6 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
_diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_actionContextAccessor = actionContextAccessor ?? throw new ArgumentNullException(nameof(actionContextAccessor));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_actionContext = actionContext ?? throw new ArgumentNullException(nameof(actionContext));
@ -58,6 +61,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
try
{
_actionContextAccessor.ActionContext = _actionContext;
_diagnosticListener.BeforeAction(
_actionContext.ActionDescriptor,
_actionContext.HttpContext,

View File

@ -9,32 +9,25 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class ActionEndpointFactory
{
private readonly RoutePatternTransformer _routePatternTransformer;
private readonly MvcEndpointInvokerFactory _invokerFactory;
public ActionEndpointFactory(
RoutePatternTransformer routePatternTransformer,
MvcEndpointInvokerFactory invokerFactory)
public ActionEndpointFactory(RoutePatternTransformer routePatternTransformer)
{
if (routePatternTransformer == null)
{
throw new ArgumentNullException(nameof(routePatternTransformer));
}
if (invokerFactory == null)
{
throw new ArgumentNullException(nameof(invokerFactory));
}
_routePatternTransformer = routePatternTransformer;
_invokerFactory = invokerFactory;
}
public void AddEndpoints(
@ -188,13 +181,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
bool suppressPathMatching,
IReadOnlyList<Action<EndpointBuilder>> conventions)
{
// We don't want to close over the retrieve the Invoker Factory in ActionEndpointFactory as
// that creates cycles in DI. Since we're creating this delegate at startup time
// we don't want to create all of the things we use at runtime until the action
// actually matches.
//
// The request delegate is already a closure here because we close over
// the action descriptor.
IActionInvokerFactory invokerFactory = null;
RequestDelegate requestDelegate = (context) =>
{
var routeData = context.GetRouteData();
var actionContext = new ActionContext(context, routeData, action);
var invoker = _invokerFactory.CreateInvoker(actionContext);
if (invokerFactory == null)
{
invokerFactory = context.RequestServices.GetRequiredService<IActionInvokerFactory>();
}
var invoker = invokerFactory.CreateInvoker(actionContext);
return invoker.InvokeAsync();
};

View File

@ -14,32 +14,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class MvcAttributeRouteHandler : IRouter
{
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IActionInvokerFactory _actionInvokerFactory;
private readonly IActionSelector _actionSelector;
private readonly ILogger _logger;
private DiagnosticListener _diagnosticListener;
private readonly DiagnosticListener _diagnosticListener;
public MvcAttributeRouteHandler(
IActionInvokerFactory actionInvokerFactory,
IActionSelector actionSelector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory)
: this(actionInvokerFactory, actionSelector, diagnosticListener, loggerFactory, actionContextAccessor: null)
{
}
public MvcAttributeRouteHandler(
IActionInvokerFactory actionInvokerFactory,
IActionSelector actionSelector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory,
IActionContextAccessor actionContextAccessor)
{
// The IActionContextAccessor is optional. We want to avoid the overhead of using CallContext
// if possible.
_actionContextAccessor = actionContextAccessor;
_actionInvokerFactory = actionInvokerFactory;
_actionSelector = actionSelector;
_diagnosticListener = diagnosticListener;
@ -94,11 +79,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var routeData = c.GetRouteData();
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
if (_actionContextAccessor != null)
{
_actionContextAccessor.ActionContext = actionContext;
}
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{

View File

@ -1,41 +0,0 @@
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Routing
{
internal sealed class MvcEndpointInvokerFactory : IActionInvokerFactory
{
private readonly IActionInvokerFactory _invokerFactory;
private readonly IActionContextAccessor _actionContextAccessor;
public MvcEndpointInvokerFactory(
IActionInvokerFactory invokerFactory)
: this(invokerFactory, actionContextAccessor: null)
{
}
public MvcEndpointInvokerFactory(
IActionInvokerFactory invokerFactory,
IActionContextAccessor actionContextAccessor)
{
_invokerFactory = invokerFactory;
// The IActionContextAccessor is optional. We want to avoid the overhead of using CallContext
// if possible.
_actionContextAccessor = actionContextAccessor;
}
public IActionInvoker CreateInvoker(ActionContext actionContext)
{
if (_actionContextAccessor != null)
{
_actionContextAccessor.ActionContext = actionContext;
}
return _invokerFactory.CreateInvoker(actionContext);
}
}
}

View File

@ -13,7 +13,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class MvcRouteHandler : IRouter
{
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IActionInvokerFactory _actionInvokerFactory;
private readonly IActionSelector _actionSelector;
private readonly ILogger _logger;
@ -24,21 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
IActionSelector actionSelector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory)
: this(actionInvokerFactory, actionSelector, diagnosticListener, loggerFactory, actionContextAccessor: null)
{
}
public MvcRouteHandler(
IActionInvokerFactory actionInvokerFactory,
IActionSelector actionSelector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory,
IActionContextAccessor actionContextAccessor)
{
// The IActionContextAccessor is optional. We want to avoid the overhead of using CallContext
// if possible.
_actionContextAccessor = actionContextAccessor;
_actionInvokerFactory = actionInvokerFactory;
_actionSelector = actionSelector;
_diagnosticListener = diagnosticListener;
@ -82,11 +67,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var routeData = c.GetRouteData();
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
if (_actionContextAccessor != null)
{
_actionContextAccessor.ActionContext = actionContext;
}
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages
@ -59,5 +60,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
/// Gets or sets the <see cref="TypeInfo"/> of the page.
/// </summary>
public TypeInfo PageTypeInfo { get; set; }
/// <summary>
/// Gets or sets the associated <see cref="Endpoint"/> of this page.
/// </summary>
public Endpoint Endpoint { get; set; }
}
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Resources = Microsoft.AspNetCore.Mvc.RazorPages.Resources;
@ -82,6 +83,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorPagesRazorViewEngineOptionsSetup>());
// Routing
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, PageLoaderMatcherPolicy>());
// Action description and invocation
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());

View File

@ -5,10 +5,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
@ -17,12 +19,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
private readonly IPageApplicationModelProvider[] _applicationModelProviders;
private readonly IViewCompilerProvider _viewCompilerProvider;
private readonly ActionEndpointFactory _endpointFactory;
private readonly PageConventionCollection _conventions;
private readonly FilterCollection _globalFilters;
public DefaultPageLoader(
IEnumerable<IPageApplicationModelProvider> applicationModelProviders,
IViewCompilerProvider viewCompilerProvider,
ActionEndpointFactory endpointFactory,
IOptions<RazorPagesOptions> pageOptions,
IOptions<MvcOptions> mvcOptions)
{
@ -30,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
.OrderBy(p => p.Order)
.ToArray();
_viewCompilerProvider = viewCompilerProvider;
_endpointFactory = endpointFactory;
_conventions = pageOptions.Value.Conventions;
_globalFilters = mvcOptions.Value.Filters;
}
@ -59,7 +64,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
ApplyConventions(_conventions, context.PageApplicationModel);
return 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...
// 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
// with all of the information we get from the compiled action descriptor.
var endpoints = new List<Endpoint>();
_endpointFactory.AddEndpoints(endpoints, compiled, Array.Empty<ConventionalRouteEntry>(), Array.Empty<Action<EndpointBuilder>>());
// In some test scenarios there's no route so the endpoint isn't created. This is fine because
// it won't happen for real.
compiled.Endpoint = endpoints.SingleOrDefault();
return compiled;
}
internal static void ApplyConventions(

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
@ -106,6 +107,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
descriptor.RouteValues.Add("page", model.ViewEnginePath);
}
// Mark all pages as a "dynamic endpoint" - this is how we deal with the compilation of pages
// in endpoint routing.
descriptor.EndpointMetadata.Add(new DynamicEndpointMetadata());
actions.Add(descriptor);
}
}
@ -138,5 +143,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
// Combine transformed page route with template
return AttributeRouteModel.CombineTemplates(transformedPageRoute, pageRouteMetadata.RouteTemplate);
}
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
{
public bool IsDynamic => true;
}
}
}

View File

@ -41,6 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
IPageHandlerMethodSelector handlerMethodSelector,
DiagnosticListener diagnosticListener,
ILogger logger,
IActionContextAccessor actionContextAccessor,
IActionResultTypeMapper mapper,
PageContext pageContext,
IFilterMetadata[] filterMetadata,
@ -51,6 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
: base(
diagnosticListener,
logger,
actionContextAccessor,
mapper,
pageContext,
filterMetadata,

View File

@ -38,6 +38,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
private readonly DiagnosticListener _diagnosticListener;
private readonly ILogger<PageActionInvoker> _logger;
private readonly IActionResultTypeMapper _mapper;
private readonly IActionContextAccessor _actionContextAccessor;
private volatile InnerCache _currentCache;
public PageActionInvokerProvider(
@ -57,6 +59,45 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory,
IActionResultTypeMapper mapper)
: this(
loader,
pageFactoryProvider,
modelFactoryProvider,
razorPageFactoryProvider,
collectionProvider,
filterProviders,
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
tempDataFactory,
mvcOptions,
htmlHelperOptions,
selector,
diagnosticListener,
loggerFactory,
mapper,
actionContextAccessor: null)
{
}
public PageActionInvokerProvider(
IPageLoader loader,
IPageFactoryProvider pageFactoryProvider,
IPageModelFactoryProvider modelFactoryProvider,
IRazorPageFactoryProvider razorPageFactoryProvider,
IActionDescriptorCollectionProvider collectionProvider,
IEnumerable<IFilterProvider> filterProviders,
ParameterBinder parameterBinder,
IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
ITempDataDictionaryFactory tempDataFactory,
IOptions<MvcOptions> mvcOptions,
IOptions<HtmlHelperOptions> htmlHelperOptions,
IPageHandlerMethodSelector selector,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory,
IActionResultTypeMapper mapper,
IActionContextAccessor actionContextAccessor)
{
_loader = loader;
_pageFactoryProvider = pageFactoryProvider;
@ -75,6 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
_diagnosticListener = diagnosticListener;
_logger = loggerFactory.CreateLogger<PageActionInvoker>();
_mapper = mapper;
_actionContextAccessor = actionContextAccessor ?? ActionContextAccessor.Null;
}
public int Order { get; } = -1000;
@ -154,6 +196,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
_selector,
_diagnosticListener,
_logger,
_actionContextAccessor,
_mapper,
pageContext,
filters,

View File

@ -0,0 +1,89 @@
// 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;
using System.Collections.Generic;
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;
public PageLoaderMatcherPolicy(IPageLoader loader)
{
if (loader == null)
{
throw new ArgumentNullException(nameof(loader));
}
_loader = loader;
}
public override int Order => int.MinValue + 100;
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (!ContainsDynamicEndpoints(endpoints))
{
// Pages are always dynamic endpoints.
return false;
}
for (var i = 0; i < endpoints.Count; i++)
{
var page = endpoints[i].Metadata.GetMetadata<PageActionDescriptor>();
if (page != null)
{
// Found a page
return true;
}
}
return false;
}
public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (candidates == null)
{
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 page = endpoint.Metadata.GetMetadata<PageActionDescriptor>();
if (page != null)
{
var compiled = _loader.Load(page);
candidates.ReplaceEndpoint(i, compiled.Endpoint, candidate.Values);
}
}
return Task.CompletedTask;
}
}
}

View File

@ -396,6 +396,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters
: base(
logger,
diagnosticListener,
ActionContextAccessor.Null,
mapper,
CreateControllerContext(actionContext, valueProviderFactories, maxAllowedErrorsInModelState),
CreateCacheEntry((ControllerActionDescriptor)actionContext.ActionDescriptor, controllerFactory),

View File

@ -1343,6 +1343,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var invoker = new ControllerActionInvoker(
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
new DiagnosticListener("Microsoft.AspNetCore"),
ActionContextAccessor.Null,
new ActionResultTypeMapper(),
controllerContext,
cacheEntry,
@ -1623,6 +1624,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var invoker = new ControllerActionInvoker(
logger,
diagnosticSource,
ActionContextAccessor.Null,
new ActionResultTypeMapper(),
controllerContext,
cacheEntry,

View File

@ -148,9 +148,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var serviceProvider = services.BuildServiceProvider();
var endpointFactory = new ActionEndpointFactory(
serviceProvider.GetRequiredService<RoutePatternTransformer>(),
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())));
var endpointFactory = new ActionEndpointFactory(serviceProvider.GetRequiredService<RoutePatternTransformer>());
return CreateDataSource(actions, endpointFactory);
}

View File

@ -32,112 +32,13 @@ namespace Microsoft.AspNetCore.Mvc.Routing
});
Services = serviceCollection.BuildServiceProvider();
InvokerFactory = new Mock<IActionInvokerFactory>(MockBehavior.Strict);
Factory = new ActionEndpointFactory(
Services.GetRequiredService<RoutePatternTransformer>(),
new MvcEndpointInvokerFactory(InvokerFactory.Object));
Factory = new ActionEndpointFactory(Services.GetRequiredService<RoutePatternTransformer>());
}
internal ActionEndpointFactory Factory { get; }
internal Mock<IActionInvokerFactory> InvokerFactory { get; }
internal IServiceProvider Services { get; }
[Fact]
public async Task AddEndpoints_AttributeRouted_UsesActionInvoker()
{
// Arrange
var values = new
{
action = "Test",
controller = "Test",
page = (string)null,
};
var action = CreateActionDescriptor(values, pattern: "/Test");
var endpointFeature = new EndpointSelectorContext
{
RouteValues = new RouteValueDictionary()
};
var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(endpointFeature);
featureCollection.Set<IRouteValuesFeature>(endpointFeature);
featureCollection.Set<IRoutingFeature>(endpointFeature);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
var actionInvokerCalled = false;
var actionInvokerMock = new Mock<IActionInvoker>();
actionInvokerMock.Setup(m => m.InvokeAsync()).Returns(() =>
{
actionInvokerCalled = true;
return Task.CompletedTask;
});
InvokerFactory
.Setup(m => m.CreateInvoker(It.IsAny<ActionContext>()))
.Returns(actionInvokerMock.Object);
// Act
var endpoint = CreateAttributeRoutedEndpoint(action);
// Assert
await endpoint.RequestDelegate(httpContextMock.Object);
Assert.True(actionInvokerCalled);
}
[Fact]
public async Task AddEndpoints_ConventionalRouted_UsesActionInvoker()
{
// Arrange
var values = new
{
action = "Test",
controller = "Test",
page = (string)null,
};
var action = CreateActionDescriptor(values);
var endpointFeature = new EndpointSelectorContext
{
RouteValues = new RouteValueDictionary()
};
var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(endpointFeature);
featureCollection.Set<IRouteValuesFeature>(endpointFeature);
featureCollection.Set<IRoutingFeature>(endpointFeature);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
var actionInvokerCalled = false;
var actionInvokerMock = new Mock<IActionInvoker>();
actionInvokerMock.Setup(m => m.InvokeAsync()).Returns(() =>
{
actionInvokerCalled = true;
return Task.CompletedTask;
});
InvokerFactory
.Setup(m => m.CreateInvoker(It.IsAny<ActionContext>()))
.Returns(actionInvokerMock.Object);
// Act
var endpoint = CreateConventionalRoutedEndpoint(action, "{controller}/{action}");
// Assert
await endpoint.RequestDelegate(httpContextMock.Object);
Assert.True(actionInvokerCalled);
}
[Fact]
public void AddEndpoints_ConventionalRouted_WithEmptyRouteName_CreatesMetadataWithEmptyRouteName()
{

View File

@ -56,8 +56,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
ILoggerFactory loggerFactory = null,
object diagnosticListener = null)
{
var actionContextAccessor = new ActionContextAccessor();
if (actionDescriptor == null)
{
var mockAction = new Mock<ActionDescriptor>();
@ -105,8 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
invokerFactory,
actionSelector,
diagnosticSource,
loggerFactory,
actionContextAccessor);
loggerFactory);
}
private RouteContext CreateRouteContext()

View File

@ -4,8 +4,11 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
@ -25,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var razorPagesOptions = Options.Create(new RazorPagesOptions());
var mvcOptions = Options.Create(new MvcOptions());
var endpointFactory = new ActionEndpointFactory(Mock.Of<RoutePatternTransformer>());
var provider1 = new Mock<IPageApplicationModelProvider>();
var provider2 = new Mock<IPageApplicationModelProvider>();
@ -75,6 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var loader = new DefaultPageLoader(
providers,
compilerProvider,
endpointFactory,
razorPagesOptions,
mvcOptions);
@ -86,6 +91,60 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
provider2.Verify();
}
[Fact]
public void Load_CreatesEndpoint_WithRoute()
{
// 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(
providers,
compilerProvider,
endpointFactory,
razorPagesOptions,
mvcOptions);
// Act
var result = loader.Load(descriptor);
// Assert
Assert.NotNull(result.Endpoint);
}
[Fact]
public void Load_InvokesApplicationModelProviders_WithTheRightOrder()
{
@ -94,6 +153,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var compilerProvider = GetCompilerProvider();
var razorPagesOptions = Options.Create(new RazorPagesOptions());
var mvcOptions = Options.Create(new MvcOptions());
var endpointFactory = new ActionEndpointFactory(Mock.Of<RoutePatternTransformer>());
var provider1 = new Mock<IPageApplicationModelProvider>();
provider1.SetupGet(p => p.Order).Returns(10);
@ -138,6 +198,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var loader = new DefaultPageLoader(
providers,
compilerProvider,
endpointFactory,
razorPagesOptions,
mvcOptions);

View File

@ -232,8 +232,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal(model.RelativePath, descriptor.RelativePath);
var actual = Assert.Single(descriptor.EndpointMetadata);
Assert.Same(expected, actual);
Assert.Single(descriptor.EndpointMetadata, expected);
}
[Fact]

View File

@ -1519,6 +1519,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
selector.Object,
diagnosticListener ?? new DiagnosticListener("Microsoft.AspNetCore"),
logger ?? NullLogger.Instance,
ActionContextAccessor.Null,
new ActionResultTypeMapper(),
pageContext,
filters ?? Array.Empty<IFilterMetadata>(),

View File

@ -46,13 +46,6 @@ namespace Microsoft.AspNetCore.Authorization
var endpoint = context.GetEndpoint();
// Workaround for https://github.com/aspnet/AspNetCore/issues/7011. Do not use the AuthorizationMiddleware for Razor Pages
if (endpoint != null && endpoint.Metadata.Any(m => m.GetType().FullName == "Microsoft.AspNetCore.Mvc.ApplicationModels.PageRouteMetadata"))
{
await _next(context);
return;
}
// Flag to indicate to other systems, e.g. MVC, that authorization middleware was run for this request
context.Items[AuthorizationMiddlewareInvokedKey] = AuthorizationMiddlewareInvokedValue;