File: RouteValuesAddressScheme.cs
Web Access
Project: src\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;
        }
    }
}