// 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.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Logging; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing.Tree { /// /// An implementation for attribute routing. /// public class TreeRouter : IRouter { // Key used by routing and action selection to match an attribute route entry to a // group of action descriptors. public static readonly string RouteGroupKey = "!__route_group"; private readonly LinkGenerationDecisionTree _linkGenerationTree; private readonly UrlMatchingTree[] _trees; private readonly IDictionary _namedEntries; private readonly ILogger _logger; private readonly ILogger _constraintLogger; /// /// Creates a new . /// /// The list of that contains the route entries. /// The set of . /// The . /// The . /// The instance. /// The instance used /// in . /// The version of this route. public TreeRouter( UrlMatchingTree[] trees, IEnumerable linkGenerationEntries, UrlEncoder urlEncoder, ObjectPool objectPool, ILogger routeLogger, ILogger constraintLogger, int version) { if (trees == null) { throw new ArgumentNullException(nameof(trees)); } if (linkGenerationEntries == null) { throw new ArgumentNullException(nameof(linkGenerationEntries)); } if (urlEncoder == null) { throw new ArgumentNullException(nameof(urlEncoder)); } if (objectPool == null) { throw new ArgumentNullException(nameof(objectPool)); } if (routeLogger == null) { throw new ArgumentNullException(nameof(routeLogger)); } if (constraintLogger == null) { throw new ArgumentNullException(nameof(constraintLogger)); } _trees = trees; _logger = routeLogger; _constraintLogger = constraintLogger; _namedEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); var outboundMatches = new List(); foreach (var entry in linkGenerationEntries) { var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; outboundMatches.Add(outboundMatch); // Skip unnamed entries if (entry.RouteName == null) { continue; } // We only need to keep one OutboundMatch per route template // so in case two entries have the same name and the same template we only keep // the first entry. OutboundMatch namedMatch; if (_namedEntries.TryGetValue(entry.RouteName, out namedMatch) && !string.Equals( namedMatch.Entry.RouteTemplate.TemplateText, entry.RouteTemplate.TemplateText, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException( Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName), nameof(linkGenerationEntries)); } else if (namedMatch == null) { _namedEntries.Add(entry.RouteName, outboundMatch); } } // The decision tree will take care of ordering for these entries. _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray()); Version = version; } /// /// Gets the version of this route. /// public int Version { get; } internal IEnumerable MatchingTrees => _trees; /// public VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // If it's a named route we will try to generate a link directly and // if we can't, we will not try to generate it using an unnamed route. if (context.RouteName != null) { return GetVirtualPathForNamedRoute(context); } // The decision tree will give us back all entries that match the provided route data in the correct // order. We just need to iterate them and use the first one that can generate a link. var matches = _linkGenerationTree.GetMatches(context.Values, context.AmbientValues); if (matches == null) { return null; } for (var i = 0; i < matches.Count; i++) { var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder); if (path != null) { return path; } } return null; } /// public async Task RouteAsync(RouteContext context) { foreach (var tree in _trees) { var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); var root = tree.Root; var treeEnumerator = new TreeEnumerator(root, tokenizer); // Create a snapshot before processing the route. We'll restore this snapshot before running each // to restore the state. This is likely an "empty" snapshot, which doesn't allocate. var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null); while (treeEnumerator.MoveNext()) { var node = treeEnumerator.Current; foreach (var item in node.Matches) { var entry = item.Entry; var matcher = item.TemplateMatcher; try { if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) { continue; } if (!RouteConstraintMatcher.Match( entry.Constraints, context.RouteData.Values, context.HttpContext, this, RouteDirection.IncomingRequest, _constraintLogger)) { continue; } _logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText); context.RouteData.Routers.Add(entry.Handler); await entry.Handler.RouteAsync(context); if (context.Handler != null) { return; } } finally { if (context.Handler == null) { // Restore the original values to prevent polluting the route data. snapshot.Restore(); } } } } } } private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) { OutboundMatch match; if (_namedEntries.TryGetValue(context.RouteName, out match)) { var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); if (path != null) { return path; } } return null; } private VirtualPathData GenerateVirtualPath( VirtualPathContext context, OutboundRouteEntry entry, TemplateBinder binder) { // In attribute the context includes the values that are used to select this entry - typically // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't // want to pass these to the link generation code, or else they will end up as query parameters. // // So, we need to exclude from here any values that are 'required link values', but aren't // parameters in the template. // // Ex: // template: api/Products/{action} // required values: { id = "5", action = "Buy", Controller = "CoolProducts" } // // result: { id = "5", action = "Buy" } var inputValues = new RouteValueDictionary(); foreach (var kvp in context.Values) { if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) { var parameter = entry.RouteTemplate.GetParameter(kvp.Key); if (parameter == null) { continue; } } inputValues.Add(kvp.Key, kvp.Value); } var bindingResult = binder.GetValues(context.AmbientValues, inputValues); if (bindingResult == null) { // A required parameter in the template didn't get a value. return null; } var matched = RouteConstraintMatcher.Match( entry.Constraints, bindingResult.CombinedValues, context.HttpContext, this, RouteDirection.UrlGeneration, _constraintLogger); if (!matched) { // A constraint rejected this link. return null; } var pathData = entry.Handler.GetVirtualPath(context); if (pathData != null) { // If path is non-null then the target router short-circuited, we don't expect this // in typical MVC scenarios. return pathData; } var path = binder.BindValues(bindingResult.AcceptedValues); if (path == null) { return null; } return new VirtualPathData(this, path); } } }