| File: Infrastructure\DynamicPageEndpointMatcherPolicy.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.RazorPages\src\Microsoft.AspNetCore.Mvc.RazorPages.csproj (Microsoft.AspNetCore.Mvc.RazorPages) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; internal sealed class DynamicPageEndpointMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy { private readonly DynamicPageEndpointSelectorCache _selectorCache; private readonly PageLoader _loader; private readonly EndpointMetadataComparer _comparer; public DynamicPageEndpointMatcherPolicy(DynamicPageEndpointSelectorCache selectorCache, PageLoader loader, EndpointMetadataComparer comparer) { ArgumentNullException.ThrowIfNull(selectorCache); ArgumentNullException.ThrowIfNull(loader); ArgumentNullException.ThrowIfNull(comparer); _selectorCache = selectorCache; _loader = loader; _comparer = comparer; } public override int Order => int.MinValue + 100; public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints) { ArgumentNullException.ThrowIfNull(endpoints); if (!ContainsDynamicEndpoints(endpoints)) { // Dynamic page endpoints are always dynamic endpoints. return false; } for (var i = 0; i < endpoints.Count; i++) { if (endpoints[i].Metadata.GetMetadata<DynamicPageMetadata>() != null) { // Found a dynamic page endpoint return true; } if (endpoints[i].Metadata.GetMetadata<DynamicPageRouteValueTransformerMetadata>() != null) { // Found a dynamic page endpoint return true; } } return false; } public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(candidates); DynamicPageEndpointSelector? selector = null; // There's no real benefit here from trying to avoid the async state machine. // We only execute on nodes that contain a dynamic policy, and thus always have // to await something. for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var endpoint = candidates[i].Endpoint; var originalValues = candidates[i].Values; RouteValueDictionary? dynamicValues = null; // We don't expect both of these to be provided, and they are internal so there's // no realistic way this could happen. var dynamicPageMetadata = endpoint.Metadata.GetMetadata<DynamicPageMetadata>(); var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicPageRouteValueTransformerMetadata>(); DynamicRouteValueTransformer? transformer = null; if (dynamicPageMetadata != null) { dynamicValues = dynamicPageMetadata.Values; } else if (transformerMetadata != null) { transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType); if (transformer.State != null) { throw new InvalidOperationException(Resources.FormatStateShouldBeNullForRouteValueTransformers(transformerMetadata.SelectorType.Name)); } transformer.State = transformerMetadata.State; dynamicValues = await transformer.TransformAsync(httpContext, originalValues!); } else { // Not a dynamic page continue; } if (dynamicValues == null) { candidates.ReplaceEndpoint(i, null, null); continue; } selector = ResolveSelector(selector, endpoint); var endpoints = selector.SelectEndpoints(dynamicValues); if (endpoints.Count == 0 && dynamicPageMetadata != null) { // Having no match for a fallback is a configuration error. We can't really check // during startup that the action you configured exists, so this is the best we can do. throw new InvalidOperationException( "Cannot find the fallback endpoint specified by route values: " + "{ " + string.Join(", ", dynamicValues.Select(kvp => $"{kvp.Key}: {kvp.Value}")) + " }."); } else if (endpoints.Count == 0) { candidates.ReplaceEndpoint(i, null, null); continue; } // We need to provide the route values associated with this endpoint, so that features // like URL generation work. var values = new RouteValueDictionary(dynamicValues); // Include values that were matched by the fallback route. if (originalValues != null) { foreach (var kvp in originalValues) { values.TryAdd(kvp.Key, kvp.Value); } } if (transformer != null) { endpoints = await transformer.FilterAsync(httpContext, values, endpoints); if (endpoints.Count == 0) { candidates.ReplaceEndpoint(i, null, null); continue; } } // Update the route values candidates.ReplaceEndpoint(i, endpoint, values); var loadedEndpoints = new List<Endpoint>(endpoints); for (var j = 0; j < loadedEndpoints.Count; j++) { var metadata = loadedEndpoints[j].Metadata; var actionDescriptor = metadata.GetMetadata<PageActionDescriptor>(); if (actionDescriptor is CompiledPageActionDescriptor) { // Nothing to do here. The endpoint already represents a compiled page. } else { // We're working with a runtime-compiled page and have to Load it. var compiled = actionDescriptor!.CompiledPageDescriptor ?? await _loader.LoadAsync(actionDescriptor, endpoint.Metadata); loadedEndpoints[j] = compiled.Endpoint!; } } // Expand the list of endpoints candidates.ExpandEndpoint(i, loadedEndpoints, _comparer); } } private DynamicPageEndpointSelector ResolveSelector(DynamicPageEndpointSelector? currentSelector, Endpoint endpoint) { var selector = _selectorCache.GetEndpointSelector(endpoint); Debug.Assert(currentSelector == null || ReferenceEquals(currentSelector, selector)); return selector!; } }