File: Routing\RouteTableFactory.cs
Web Access
Project: src\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components)
// 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.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
using Microsoft.AspNetCore.Routing.Constraints;
 
namespace Microsoft.AspNetCore.Components;
 
/// <summary>
/// Resolves components for an application.
/// </summary>
internal class RouteTableFactory
{
    public static readonly RouteTableFactory Instance = new();
    public static readonly IComparer<InboundRouteEntry> RouteOrder = Comparer<InboundRouteEntry>.Create((x, y) =>
    {
        var result = RouteComparison(x, y);
        return result != 0 ? result : string.Compare(x.RoutePattern.RawText, y.RoutePattern.RawText, StringComparison.OrdinalIgnoreCase);
    });
 
    private readonly ConcurrentDictionary<RouteKey, RouteTable> _cache = new();
 
    public RouteTable Create(RouteKey routeKey, IServiceProvider serviceProvider)
    {
        if (_cache.TryGetValue(routeKey, out var resolvedComponents))
        {
            return resolvedComponents;
        }
 
        var componentTypes = GetRouteableComponents(routeKey);
        var routeTable = Create(componentTypes, serviceProvider);
        _cache.TryAdd(routeKey, routeTable);
        return routeTable;
    }
 
    public void ClearCaches() => _cache.Clear();
 
    [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
    private static List<Type> GetRouteableComponents(RouteKey routeKey)
    {
        var routeableComponents = new List<Type>();
        if (routeKey.AppAssembly is not null)
        {
            GetRouteableComponents(routeableComponents, routeKey.AppAssembly);
        }
 
        if (routeKey.AdditionalAssemblies is not null)
        {
            foreach (var assembly in routeKey.AdditionalAssemblies)
            {
                // We don't need process the assembly if it's the app assembly.
                if (assembly != routeKey.AppAssembly)
                {
                    GetRouteableComponents(routeableComponents, assembly);
                }
            }
        }
 
        return routeableComponents;
 
        static void GetRouteableComponents(List<Type> routeableComponents, Assembly assembly)
        {
            foreach (var type in assembly.ExportedTypes)
            {
                if (typeof(IComponent).IsAssignableFrom(type)
                    && type.IsDefined(typeof(RouteAttribute))
                    && !type.IsDefined(typeof(ExcludeFromInteractiveRoutingAttribute)))
                {
                    routeableComponents.Add(type);
                }
            }
        }
    }
 
    internal static RouteTable Create(List<Type> componentTypes, IServiceProvider serviceProvider)
    {
        var templatesByHandler = new Dictionary<Type, string[]>();
        foreach (var componentType in componentTypes)
        {
            // We're deliberately using inherit = false here.
            //
            // RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
            // ambiguity. You end up with two components (base class and derived class) with the same route.
            var templates = GetTemplates(componentType);
 
            templatesByHandler.Add(componentType, templates);
        }
        return Create(templatesByHandler, serviceProvider);
    }
 
    private static string[] GetTemplates(Type componentType)
    {
        var routeAttributes = componentType.GetCustomAttributes(typeof(RouteAttribute), inherit: false);
        var templates = new string[routeAttributes.Length];
        for (var i = 0; i < routeAttributes.Length; i++)
        {
            var attribute = (RouteAttribute)routeAttributes[i];
            templates[i] = attribute.Template;
        }
 
        return templates;
    }
 
    [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
    internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider)
    {
        var routeOptions = Options.Create(new RouteOptions());
        if (!OperatingSystem.IsBrowser() || RegexConstraintSupport.IsEnabled)
        {
            routeOptions.Value.SetParameterPolicy("regex", typeof(RegexInlineRouteConstraint));
        }
        var builder = new TreeRouteBuilder(
            serviceProvider.GetRequiredService<ILoggerFactory>(),
            new DefaultInlineConstraintResolver(routeOptions, serviceProvider));
 
        foreach (var (type, templates) in templatesByHandler)
        {
            var result = ComputeTemplateGroupInfo(templates);
 
            var parsedTemplates = result.ParsedTemplates;
            var allRouteParameterNames = result.AllRouteParameterNames;
 
            foreach (var (parsedTemplate, routeParameterNames) in parsedTemplates)
            {
                var unusedRouteParameterNames = GetUnusedParameterNames(allRouteParameterNames!, routeParameterNames!);
                builder.MapInbound(type, parsedTemplate, unusedRouteParameterNames);
            }
        }
 
        DetectAmbiguousRoutes(builder);
 
        return new RouteTable(builder.Build());
    }
 
    private static TemplateGroupInfo ComputeTemplateGroupInfo(string[] templates)
    {
        var result = new TemplateGroupInfo(templates);
        for (var i = 0; i < templates.Length; i++)
        {
            var parsedTemplate = RoutePatternParser.Parse(templates[i]);
            var parameterNames = GetParameterNames(parsedTemplate);
            result.ParsedTemplates[i] = (parsedTemplate, parameterNames);
 
            foreach (var parameterName in parameterNames)
            {
                result.AllRouteParameterNames.Add(parameterName);
            }
        }
 
        return result;
    }
 
    private struct TemplateGroupInfo(string[] templates)
    {
        public HashSet<string> AllRouteParameterNames { get; set; } = new(StringComparer.OrdinalIgnoreCase);
        public (RoutePattern, HashSet<string>)[] ParsedTemplates { get; set; } = new (RoutePattern, HashSet<string>)[templates.Length];
    }
 
    internal static InboundRouteEntry CreateEntry([DynamicallyAccessedMembers(Component)] Type pageType, string template)
    {
        var templates = GetTemplates(pageType);
        var result = ComputeTemplateGroupInfo(templates);
 
        RoutePattern? parsedTemplate = null;
        HashSet<string>? routeParameterNames = null;
        for (var i = 0; i < result.ParsedTemplates.Length; i++)
        {
            var (parsed, parameters) = result.ParsedTemplates[i];
            if (string.Equals(parsed.RawText, template, StringComparison.OrdinalIgnoreCase))
            {
                parsedTemplate = parsed;
                routeParameterNames = parameters;
                break;
            }
        }
 
        if (parsedTemplate == null)
        {
            throw new InvalidOperationException($"Unable to find the provided template '{template}'");
        }
 
        return new InboundRouteEntry()
        {
            Handler = pageType,
            RoutePattern = parsedTemplate,
            UnusedRouteParameterNames = GetUnusedParameterNames(result.AllRouteParameterNames, routeParameterNames!),
        };
    }
 
    private static void DetectAmbiguousRoutes(TreeRouteBuilder builder)
    {
        for (var i = 0; i < builder.InboundEntries.Count; i++)
        {
            var left = builder.InboundEntries[i];
            for (var j = i + 1; j < builder.InboundEntries.Count; j++)
            {
                var right = builder.InboundEntries[j];
                var leftText = left.RoutePattern.RawText!.Trim('/');
                var rightText = right.RoutePattern.RawText!.Trim('/');
                if (left.Precedence != right.Precedence)
                {
                    continue;
                }
 
                var ambiguous = CompareSegments(left, right);
                if (ambiguous)
                {
                    throw new InvalidOperationException($@"The following routes are ambiguous:
'{leftText}' in '{left.Handler.FullName}'
'{rightText}' in '{right.Handler.FullName}'
");
                }
            }
        }
    }
 
    private static bool CompareSegments(InboundRouteEntry left, InboundRouteEntry right)
    {
        var ambiguous = true;
        for (var k = 0; k < left.RoutePattern.PathSegments.Count; k++)
        {
            var leftSegment = left.RoutePattern.PathSegments[k];
            var rightSegment = right.RoutePattern.PathSegments[k];
            if (leftSegment.Parts.Count != rightSegment.Parts.Count)
            {
                ambiguous = false;
                break;
            }
 
            for (var l = 0; l < leftSegment.Parts.Count; l++)
            {
                var leftPart = leftSegment.Parts[l];
                var rightPart = rightSegment.Parts[l];
                if (leftPart is RoutePatternLiteralPart leftLiteral &&
                    rightPart is RoutePatternLiteralPart rightLiteral &&
                    !string.Equals(leftLiteral.Content, rightLiteral.Content, StringComparison.OrdinalIgnoreCase))
                {
                    ambiguous = false;
                    break;
                }
            }
            if (!ambiguous)
            {
                break;
            }
        }
 
        return ambiguous;
    }
 
    private static HashSet<string> GetParameterNames(RoutePattern routeTemplate)
    {
        var parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var parameter in routeTemplate.Parameters)
        {
            parameterNames.Add(parameter.Name!);
        }
 
        return parameterNames;
    }
 
    private static List<string>? GetUnusedParameterNames(HashSet<string> allRouteParameterNames, HashSet<string> routeParameterNames)
    {
        List<string>? unusedParameters = null;
        foreach (var item in allRouteParameterNames)
        {
            if (!routeParameterNames.Contains(item))
            {
                unusedParameters ??= new();
                unusedParameters.Add(item);
            }
        }
 
        return unusedParameters;
    }
 
    /// <summary>
    /// Route precedence algorithm.
    /// We collect all the routes and sort them from most specific to
    /// less specific. The specificity of a route is given by the specificity
    /// of its segments and the position of those segments in the route.
    /// * A literal segment is more specific than a parameter segment.
    /// * A parameter segment with more constraints is more specific than one with fewer constraints
    /// * Segment earlier in the route are evaluated before segments later in the route.
    /// For example:
    /// /Literal is more specific than /Parameter
    /// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
    /// /Product/{id:int} is more specific than /Product/{id}
    ///
    /// Routes can be ambiguous if:
    /// They are composed of literals and those literals have the same values (case insensitive)
    /// They are composed of a mix of literals and parameters, in the same relative order and the
    /// literals have the same values.
    /// For example:
    /// * /literal and /Literal
    /// /{parameter}/literal and /{something}/literal
    /// /{parameter:constraint}/literal and /{something:constraint}/literal
    ///
    /// To calculate the precedence we sort the list of routes as follows:
    /// * Shorter routes go first.
    /// * A literal wins over a parameter in precedence.
    /// * For literals with different values (case insensitive) we choose the lexical order
    /// * For parameters with different numbers of constraints, the one with more wins
    /// If we get to the end of the comparison routing we've detected an ambiguous pair of routes.
    /// </summary>
    internal static int RouteComparison(InboundRouteEntry x, InboundRouteEntry y)
    {
        if (ReferenceEquals(x, y))
        {
            return 0;
        }
 
        var xTemplate = x.RoutePattern;
        var yTemplate = y.RoutePattern;
        var xPrecedence = RoutePrecedence.ComputeInbound(xTemplate);
        var yPrecedence = RoutePrecedence.ComputeInbound(yTemplate);
 
        return (yPrecedence.CompareTo(xPrecedence)) switch
        {
            -1 => 1,
            1 => -1,
            0 => 0,
            _ => throw new InvalidOperationException("Invalid comparison result."),
        };
    }
}