| File: RouteValuesAddressScheme.cs | Web Access |
| Project: src\aspnetcore\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Frozen; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; namespace Microsoft.AspNetCore.Routing; internal sealed class RouteValuesAddressScheme : IEndpointAddressScheme<RouteValuesAddress>, IDisposable { private readonly DataSourceDependentCache<StateEntry> _cache; public RouteValuesAddressScheme(EndpointDataSource dataSource) { _cache = new DataSourceDependentCache<StateEntry>(dataSource, Initialize); } // Internal for tests internal StateEntry State => _cache.EnsureInitialized(); public IEnumerable<Endpoint> FindEndpoints(RouteValuesAddress address) { ArgumentNullException.ThrowIfNull(address); var state = State; IList<OutboundMatchResult>? matchResults = null; if (string.IsNullOrEmpty(address.RouteName)) { matchResults = state.AllMatchesLinkGenerationTree.GetMatches( address.ExplicitValues, address.AmbientValues); } else if (state.NamedMatches.TryGetValue(address.RouteName, out var namedMatchResults)) { matchResults = namedMatchResults; } if (matchResults != null) { var matchCount = matchResults.Count; if (matchCount > 0) { if (matchResults.Count == 1) { // Special case having a single result to avoid creating iterator state machine return new[] { (RouteEndpoint)matchResults[0].Match.Entry.Data }; } else { // Use separate method since one cannot have regular returns in an iterator method return GetEndpoints(matchResults, matchCount); } } } return Array.Empty<Endpoint>(); } private static IEnumerable<Endpoint> GetEndpoints(IList<OutboundMatchResult> matchResults, int matchCount) { for (var i = 0; i < matchCount; i++) { yield return (RouteEndpoint)matchResults[i].Match.Entry.Data; } } private StateEntry Initialize(IReadOnlyList<Endpoint> endpoints) { var matchesWithRequiredValues = new List<OutboundMatch>(); var namedOutboundMatchResults = new Dictionary<string, List<OutboundMatchResult>>(StringComparer.OrdinalIgnoreCase); // Decision tree is built using the 'required values' of actions. // - When generating a url using route values, decision tree checks the explicitly supplied route values + // ambient values to see if they have a match for the required-values-based-tree. // - When generating a url using route name, route values for controller, action etc.might not be provided // (this is expected because as a user I want to avoid writing all those and instead chose to use a // routename which is quick). So since these values are not provided and might not be even in ambient // values, decision tree would fail to find a match. So for this reason decision tree is not used for named // matches. Instead all named matches are returned as is and the LinkGenerator uses a TemplateBinder to // decide which of the matches can generate a url. // For example, for a route defined like below with current ambient values like new { controller = "Home", // action = "Index" } // "api/orders/{id}", // routeName: "OrdersApi", // defaults: new { controller = "Orders", action = "GetById" }, // requiredValues: new { controller = "Orders", action = "GetById" }, // A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or // current ambient values do not satisfy the decision tree that is built based on the required values. for (var i = 0; i < endpoints.Count; i++) { var endpoint = endpoints[i]; if (!(endpoint is RouteEndpoint routeEndpoint)) { continue; } var metadata = endpoint.Metadata.GetMetadata<IRouteNameMetadata>(); if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0) { continue; } if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true) { continue; } var entry = CreateOutboundRouteEntry( routeEndpoint, routeEndpoint.RoutePattern.RequiredValues, metadata?.RouteName); var outboundMatch = new OutboundMatch() { Entry = entry }; if (routeEndpoint.RoutePattern.RequiredValues.Count > 0) { // Entries with a route name but no required values can only be matched by name. // Otherwise, these endpoints will match any attempt at action link generation. // Entries with neither a route name nor required values have already been skipped above. // See https://github.com/dotnet/aspnetcore/issues/35592 matchesWithRequiredValues.Add(outboundMatch); } if (string.IsNullOrEmpty(entry.RouteName)) { continue; } if (!namedOutboundMatchResults.TryGetValue(entry.RouteName, out var matchResults)) { matchResults = new List<OutboundMatchResult>(); namedOutboundMatchResults.Add(entry.RouteName, matchResults); } matchResults.Add(new OutboundMatchResult(outboundMatch, isFallbackMatch: false)); } return new StateEntry( matchesWithRequiredValues, new LinkGenerationDecisionTree(matchesWithRequiredValues), namedOutboundMatchResults.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); } private static OutboundRouteEntry CreateOutboundRouteEntry( RouteEndpoint endpoint, IReadOnlyDictionary<string, object?> requiredValues, string? routeName) { var entry = new OutboundRouteEntry() { Handler = NullRouter.Instance, Order = endpoint.Order, Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern), RequiredLinkValues = new RouteValueDictionary(requiredValues), RouteTemplate = new RouteTemplate(endpoint.RoutePattern), Data = endpoint, RouteName = routeName, }; entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); return entry; } public void Dispose() { _cache.Dispose(); } internal sealed class StateEntry { // For testing public readonly List<OutboundMatch> MatchesWithRequiredValues; public readonly LinkGenerationDecisionTree AllMatchesLinkGenerationTree; public readonly FrozenDictionary<string, List<OutboundMatchResult>> NamedMatches; public StateEntry( List<OutboundMatch> matchesWithRequiredValues, LinkGenerationDecisionTree allMatchesLinkGenerationTree, FrozenDictionary<string, List<OutboundMatchResult>> namedMatches) { MatchesWithRequiredValues = matchesWithRequiredValues; AllMatchesLinkGenerationTree = allMatchesLinkGenerationTree; NamedMatches = namedMatches; } } }