// 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.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.EndpointFinders; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing { internal class RouteValuesBasedEndpointFinder : IEndpointFinder { private readonly CompositeEndpointDataSource _endpointDataSource; private readonly IInlineConstraintResolver _inlineConstraintResolver; private readonly ObjectPool _objectPool; private LinkGenerationDecisionTree _allMatchesLinkGenerationTree; private IDictionary _namedMatches; public RouteValuesBasedEndpointFinder( CompositeEndpointDataSource endpointDataSource, ObjectPool objectPool, IInlineConstraintResolver inlineConstraintResolver) { _endpointDataSource = endpointDataSource; _objectPool = objectPool; _inlineConstraintResolver = inlineConstraintResolver; BuildOutboundMatches(); } public IEnumerable FindEndpoints(RouteValuesBasedEndpointFinderContext context) { IEnumerable matchResults = null; if (string.IsNullOrEmpty(context.RouteName)) { matchResults = _allMatchesLinkGenerationTree.GetMatches( context.ExplicitValues, context.AmbientValues); } else if (_namedMatches.TryGetValue(context.RouteName, out var linkGenerationTree)) { matchResults = linkGenerationTree.GetMatches( context.ExplicitValues, context.AmbientValues); } if (matchResults == null || !matchResults.Any()) { return Array.Empty(); } return matchResults .Select(matchResult => matchResult.Match) .Select(match => (MatcherEndpoint)match.Entry.Data); } private void BuildOutboundMatches() { var (allOutboundMatches, namedOutboundMatches) = GetOutboundMatches(); _namedMatches = GetNamedMatches(namedOutboundMatches); _allMatchesLinkGenerationTree = new LinkGenerationDecisionTree(allOutboundMatches.ToArray()); } private (IEnumerable, IDictionary>) GetOutboundMatches() { var allOutboundMatches = new List(); var namedOutboundMatches = new Dictionary>(StringComparer.OrdinalIgnoreCase); var endpoints = _endpointDataSource.Endpoints.OfType(); foreach (var endpoint in endpoints) { var entry = CreateOutboundRouteEntry(endpoint); var outboundMatch = new OutboundMatch() { Entry = entry }; allOutboundMatches.Add(outboundMatch); if (string.IsNullOrEmpty(entry.RouteName)) { continue; } List matches; if (!namedOutboundMatches.TryGetValue(entry.RouteName, out matches)) { matches = new List(); namedOutboundMatches.Add(entry.RouteName, matches); } matches.Add(outboundMatch); } return (allOutboundMatches, namedOutboundMatches); } private OutboundRouteEntry CreateOutboundRouteEntry(MatcherEndpoint endpoint) { var routeNameMetadata = endpoint.Metadata.GetMetadata(); var entry = new OutboundRouteEntry() { Handler = NullRouter.Instance, Order = endpoint.Order, Precedence = RoutePrecedence.ComputeOutbound(endpoint.ParsedTemplate), RequiredLinkValues = endpoint.RequiredValues, RouteTemplate = endpoint.ParsedTemplate, Data = endpoint, RouteName = routeNameMetadata?.Name, }; // TODO: review. These route constriants should be constructed when the endpoint // is built. This way they can be checked for validity on app startup too var constraintBuilder = new RouteConstraintBuilder( _inlineConstraintResolver, endpoint.ParsedTemplate.TemplateText); foreach (var parameter in endpoint.ParsedTemplate.Parameters) { if (parameter.InlineConstraints != null) { if (parameter.IsOptional) { constraintBuilder.SetOptional(parameter.Name); } foreach (var constraint in parameter.InlineConstraints) { constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); } } } entry.Constraints = constraintBuilder.Build(); entry.Defaults = endpoint.Defaults; return entry; } private IDictionary GetNamedMatches( IDictionary> namedOutboundMatches) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var namedOutboundMatch in namedOutboundMatches) { result.Add(namedOutboundMatch.Key, new LinkGenerationDecisionTree(namedOutboundMatch.Value.ToArray())); } return result; } // Used only to hook up link generation, and it doesn't need to do anything. private class NullRouter : IRouter { public static readonly NullRouter Instance = new NullRouter(); public VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; } public Task RouteAsync(RouteContext context) { throw new NotImplementedException(); } } } }