File: Routing\RouteTableFactoryTests.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Reflection;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.AspNetCore.Components.Routing;
 
public class RouteTableFactoryTests
{
    private readonly ServiceProvider _serviceProvider;
 
    public RouteTableFactoryTests()
    {
        _serviceProvider = new ServiceCollection()
            .AddLogging()
            .BuildServiceProvider();
    }
 
    [Fact]
    public void CanCacheRouteTable()
    {
        // Arrange
        var routeTableFactory = new RouteTableFactory();
        var routes1 = routeTableFactory.Create(new RouteKey(GetType().Assembly, null), _serviceProvider);
 
        // Act
        var routes2 = routeTableFactory.Create(new RouteKey(GetType().Assembly, null), _serviceProvider);
 
        // Assert
        Assert.Same(routes1, routes2);
    }
 
    [Fact]
    public void CanCacheRouteTableWithDifferentAssembliesAndOrder()
    {
        // Arrange
        var routeTableFactory = new RouteTableFactory();
        var routes1 = routeTableFactory.Create(new RouteKey(typeof(object).Assembly, new[] { typeof(ComponentBase).Assembly, GetType().Assembly, }), _serviceProvider);
 
        // Act
        var routes2 = routeTableFactory.Create(new RouteKey(typeof(object).Assembly, new[] { GetType().Assembly, typeof(ComponentBase).Assembly, }), _serviceProvider);
 
        // Assert
        Assert.Same(routes1, routes2);
    }
 
    [Fact]
    public void DoesNotCacheRouteTableForDifferentAssemblies()
    {
        // Arrange
        var routeTableFactory = new RouteTableFactory();
        var routes1 = routeTableFactory.Create(new RouteKey(GetType().Assembly, null), _serviceProvider);
 
        // Act
        var routes2 = routeTableFactory.Create(new RouteKey(GetType().Assembly, new[] { typeof(object).Assembly }), _serviceProvider);
 
        // Assert
        Assert.NotSame(routes1, routes2);
    }
 
    [Fact]
    public void IgnoresIdenticalTypes()
    {
        // Arrange & Act
        var routeTableFactory = new RouteTableFactory();
        var routeTable = routeTableFactory.Create(new RouteKey(GetType().Assembly, new[] { GetType().Assembly }), _serviceProvider);
 
        var routes = GetRoutes(routeTable);
 
        // Assert
        Assert.Equal(routes.GroupBy(x => x.Handler).Count(), routes.Count);
    }
 
    [Fact]
    public void RespectsExcludeFromInteractiveRoutingAttribute()
    {
        // Arrange & Act
        var routeTableFactory = new RouteTableFactory();
        var routeTable = routeTableFactory.Create(new RouteKey(GetType().Assembly, Array.Empty<Assembly>()), _serviceProvider);
 
        var routes = GetRoutes(routeTable);
 
        // Assert
        Assert.Contains(routes, r => r.Handler == typeof(ComponentWithoutExcludeFromInteractiveRoutingAttribute));
        Assert.DoesNotContain(routes, r => r.Handler == typeof(ComponentWithExcludeFromInteractiveRoutingAttribute));
    }
 
    [Fact]
    public void CanDiscoverRoute()
    {
        // Arrange & Act
        var routeTable = RouteTableFactory.Create(new List<Type> { typeof(MyComponent), }, _serviceProvider);
 
        var routes = GetRoutes(routeTable);
 
        // Assert
        Assert.Equal("Test1", Assert.Single(routes).RoutePattern.RawText);
    }
 
    [Route("Test1")]
    private class MyComponent : ComponentBase
    {
    }
 
    [Fact]
    public void CanDiscoverRoutes_WithInheritance()
    {
        // Arrange & Act
        var routeTable = RouteTableFactory.Create(new List<Type> { typeof(MyComponent), typeof(MyInheritedComponent) }, _serviceProvider);
 
        var routes = GetRoutes(routeTable);
 
        // Assert
        Assert.Collection(
            routes.OrderBy(r => r.RoutePattern.RawText),
            r => Assert.Equal("Test1", r.RoutePattern.RawText),
            r => Assert.Equal("Test2", r.RoutePattern.RawText));
    }
 
    private List<InboundRouteEntry> GetRoutes(RouteTable routeTable)
    {
        var matchingTree = routeTable.TreeRouter.MatchingTrees.Single();
        var result = new HashSet<InboundRouteEntry>();
        GetRoutes(matchingTree.Root, result);
        return result.ToList();
 
        void GetRoutes(UrlMatchingNode node, HashSet<InboundRouteEntry> result)
        {
            foreach (var match in node.Matches)
            {
                result.Add(match.Entry);
            }
 
            foreach (var (key, child) in node.Literals)
            {
                GetRoutes(child, result);
            }
 
            if (node.ConstrainedParameters != null)
            {
                GetRoutes(node.ConstrainedParameters, result);
            }
            if (node.Parameters != null)
            {
                GetRoutes(node.Parameters, result);
            }
 
            if (node.ConstrainedCatchAlls != null)
            {
                GetRoutes(node.ConstrainedCatchAlls, result);
            }
 
            if (node.CatchAlls != null)
            {
                GetRoutes(node.CatchAlls, result);
            }
        }
    }
 
    [Route("Test2")]
    private class MyInheritedComponent : MyComponent
    {
    }
 
    [Fact]
    public void CanMatchRootTemplate()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/").Build();
        var context = new RouteContext("/");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
    }
 
    [Fact]
    public void CanMatchLiteralTemplate()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/literal").Build();
        var context = new RouteContext("/literal/");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
    }
 
    [Fact]
    public void CanMatchTemplateWithMultipleLiterals()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/some/awesome/route/").Build();
        var context = new RouteContext("/some/awesome/route");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
    }
 
    [Fact]
    public void RouteMatchingIsCaseInsensitive()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/some/AWESOME/route/").Build();
        var context = new RouteContext("/Some/awesome/RouTe");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
    }
 
    [Fact]
    public void CanMatchEncodedSegments()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/some/ünicõdē/🛣/").Build();
        var context = new RouteContext("/some/%C3%BCnic%C3%B5d%C4%93/%F0%9F%9B%A3");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
    }
 
    [Fact]
    public void DoesNotMatchIfSegmentsDontMatch()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/some/AWESOME/route/").Build();
        var context = new RouteContext("/some/brilliant/route");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.Null(context.Handler);
    }
 
    [Theory]
    [InlineData("/{value:bool}", "/maybe")]
    [InlineData("/{value:datetime}", "/1955-01-32")]
    [InlineData("/{value:decimal}", "/hello")]
    [InlineData("/{value:double}", "/0.1.2")]
    [InlineData("/{value:float}", "/0.1.2")]
    [InlineData("/{value:guid}", "/not-a-guid")]
    [InlineData("/{value:int}", "/3.141")]
    [InlineData("/{value:long}", "/3.141")]
    public void DoesNotMatchIfConstraintDoesNotMatch(string template, string contextUrl)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
        var context = new RouteContext(contextUrl);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.Null(context.Handler);
    }
 
    [Theory]
    [InlineData("/some")]
    [InlineData("/some/awesome/route/with/extra/segments")]
    public void DoesNotMatchIfDifferentNumberOfSegments(string path)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/some/awesome/route/").Build();
        var context = new RouteContext(path);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.Null(context.Handler);
    }
 
    [Theory]
    [InlineData("/value1", "value1")]
    [InlineData("/value2/", "value2")]
    [InlineData("/d%C3%A9j%C3%A0%20vu", "déjà vu")]
    [InlineData("/d%C3%A9j%C3%A0%20vu/", "déjà vu")]
    [InlineData("/d%C3%A9j%C3%A0+vu", "déjà+vu")]
    public void CanMatchParameterTemplate(string path, string expectedValue)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/{parameter}").Build();
        var context = new RouteContext(path);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
    }
 
    [Theory]
    [InlineData("/blog/value1", "value1")]
    [InlineData("/blog/value1/foo%20bar", "value1/foo bar")]
    public void CanMatchCatchAllParameterTemplate(string path, string expectedValue)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/blog/{*parameter}").Build();
        var context = new RouteContext(path);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue);
    }
 
    [Fact]
    public void CanMatchTemplateWithMultipleParameters()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/").Build();
        var context = new RouteContext("/an/awesome/path");
 
        var expectedParameters = new Dictionary<string, object>
        {
            ["some"] = "an",
            ["route"] = "path"
        };
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Equal(expectedParameters, context.Parameters);
    }
 
    [Fact]
    public void CanMatchTemplateWithMultipleParametersAndCatchAllParameter()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/with/{*catchAll}").Build();
        var context = new RouteContext("/an/awesome/path/with/some/catch/all/stuff");
 
        var expectedParameters = new Dictionary<string, object>
        {
            ["some"] = "an",
            ["route"] = "path",
            ["catchAll"] = "some/catch/all/stuff"
        };
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Equal(expectedParameters, context.Parameters);
    }
 
    public static IEnumerable<object[]> CanMatchParameterWithConstraintCases() => new object[][]
    {
            new object[] { "/{value:bool}", "/true", true },
            new object[] { "/{value:bool}", "/false", false },
            new object[] { "/{value:datetime}", "/1955-01-30", new DateTime(1955, 1, 30) },
            new object[] { "/{value:decimal}", "/5.3", 5.3m },
            new object[] { "/{value:double}", "/0.1", 0.1d },
            new object[] { "/{value:float}", "/0.1", 0.1f },
            new object[] { "/{value:guid}", "/1FCEF085-884F-416E-B0A1-71B15F3E206B", Guid.Parse("1FCEF085-884F-416E-B0A1-71B15F3E206B", CultureInfo.InvariantCulture) },
            new object[] { "/{value:int}", "/123", 123 },
            new object[] { "/{value:int}", "/-123", -123},
            new object[] { "/{value:long}", "/9223372036854775807", long.MaxValue },
            new object[] { "/{value:long}", $"/-9223372036854775808", long.MinValue },
    };
 
    [Theory]
    [MemberData(nameof(CanMatchParameterWithConstraintCases))]
    public void CanMatchParameterWithConstraint(string template, string contextUrl, object convertedValue)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
        var context = new RouteContext(contextUrl);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        if (context.Handler == null)
        {
            // Make it easier to track down failing tests when using MemberData
            throw new InvalidOperationException($"Failed to match template '{template}'.");
        }
        Assert.Equal(new Dictionary<string, object>
            {
                { "value", convertedValue }
            }, context.Parameters);
    }
 
    [Fact]
    public void MoreSpecificRoutesPrecedeMoreGeneralRoutes()
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var builder = new TestRouteTableBuilder()
            .AddRoute("/{*last}")
            .AddRoute("/{*last:int}")
            .AddRoute("/{last}")
            .AddRoute("/{last:int}")
            .AddRoute("/literal")
            .AddRoute("/literal/{*last}")
            .AddRoute("/literal/{*last:int}")
            .AddRoute("/literal/{last}")
            .AddRoute("/literal/{last:int}")
            .AddRoute("/literal/literal");
 
        var expectedOrder = new[]
        {
                "literal",
                "literal/literal",
                "literal/{last:int}",
                "literal/{last}",
                "literal/{*last:int}",
                "literal/{*last}",
                "{last:int}",
                "{last}",
                "{*last:int}",
                "{*last}",
            };
 
        // Act
        var table = builder.Build();
 
        // Assert
        Assert.NotNull(table);
        //var tableTemplates = table.Routes.Select(p => p.Template.TemplateText).ToArray();
        //Assert.Equal(expectedOrder, tableTemplates);
    }
 
    [Theory]
    [InlineData("/literal", null, "literal", "literal/{parameter?}", typeof(TestHandler1))]
    [InlineData("/literal/value", "value", "literal", "literal/{parameter?}", typeof(TestHandler2))]
    [InlineData("/literal", null, "literal/{parameter?}", "literal/{*parameter}", typeof(TestHandler1))]
    [InlineData("/literal/value", "value", "literal/{parameter?}", "literal/{*parameter}", typeof(TestHandler1))]
    [InlineData("/literal/value/other", "value/other", "literal /{parameter?}", "literal/{*parameter}", typeof(TestHandler2))]
    public void CorrectlyMatchesVariableLengthSegments(string path, string expectedValue, string first, string second, Type handler)
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute(first, typeof(TestHandler1))
            .AddRoute(second, typeof(TestHandler2))
            .Build();
 
        var context = new RouteContext(path);
 
        // Act
        table.Route(context);
 
        // Assert
        Assert.Equal(handler, context.Handler);
        var value = expectedValue != null ? Assert.Single(context.Parameters, p => p.Key == "parameter").Value : null;
        Assert.Equal(expectedValue, value?.ToString());
    }
 
    [Theory(Skip = "Matching on a per segment basis in ASP.NET is not supported for catch-alls")]
    [InlineData("/values/{*values:int}", "/values/1/2/3/4/5")]
    [InlineData("/{*values:int}", "/1/2/3/4/5")]
    public void CanMatchCatchAllParametersWithConstraints(string template, string path)
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute(template)
            .Build();
 
        var context = new RouteContext(path);
 
        // Act
        table.Route(context);
 
        // Assert
        Assert.True(context.Parameters.TryGetValue("values", out var values));
        Assert.Equal("1/2/3/4/5", values);
    }
 
    [Fact]
    public void CatchAllEmpty()
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute("{*catchall}")
            .Build();
 
        var context = new RouteContext("/");
 
        // Act
        table.Route(context);
 
        // Assert
        Assert.True(context.Parameters.TryGetValue("catchall", out var values));
        Assert.Null(values);
    }
 
    [Fact]
    public void OptionalParameterEmpty()
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute("{parameter?}")
            .Build();
 
        var context = new RouteContext("/");
 
        // Act
        table.Route(context);
 
        // Assert
        Assert.True(context.Parameters.TryGetValue("parameter", out var values));
        Assert.Null(values);
    }
 
    [Theory]
    [InlineData("/", 0)]
    [InlineData("/1", 1)]
    [InlineData("/1/2", 2)]
    [InlineData("/1/2/3", 3)]
    public void MultipleOptionalParameters(string path, int segments)
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute("{param1?}/{param2?}/{param3?}")
            .Build();
 
        var context = new RouteContext(path);
 
        // Act
        table.Route(context);
 
        // Assert
        for (int i = 1; i <= segments; i++)
        {
            // Segments present in the path have the corresponding value.
            Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
            Assert.Equal(i.ToString(CultureInfo.InvariantCulture), value);
        }
        for (int i = segments + 1; i <= 3; i++)
        {
            // Segments omitted in the path have the default null value.
            Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
            Assert.Null(value);
        }
    }
 
    [Theory]
    [InlineData("/prefix/", 0)]
    [InlineData("/prefix/1", 1)]
    [InlineData("/prefix/1/2", 2)]
    [InlineData("/prefix/1/2/3", 3)]
    public void MultipleOptionalParametersWithPrefix(string path, int segments)
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute("prefix/{param1?}/{param2?}/{param3?}")
            .Build();
 
        var context = new RouteContext(path);
 
        // Act
        table.Route(context);
 
        // Assert
        for (int i = 1; i <= segments; i++)
        {
            // Segments present in the path have the corresponding value.
            Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
            Assert.Equal(i.ToString(CultureInfo.InvariantCulture), value);
        }
        for (int i = segments + 1; i <= 3; i++)
        {
            // Segments omitted in the path have the default null value.
            Assert.True(context.Parameters.TryGetValue($"param{i}", out var value));
            Assert.Null(value);
        }
    }
 
    [Theory]
    [InlineData("/{parameter?}/{*catchAll}", "/", null, null)]
    [InlineData("/{parameter?}/{*catchAll}", "/parameter", "parameter", null)]
    [InlineData("/{parameter?}/{*catchAll}", "/value/1", "value", "1")]
    [InlineData("/{parameter?}/{*catchAll}", "/value/1/2/3/4/5", "value", "1/2/3/4/5")]
    [InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/", null, null)]
    [InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/parameter", "parameter", null)]
    [InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/value/1", "value", "1")]
    [InlineData("prefix/{parameter?}/{*catchAll}", "/prefix/value/1/2/3/4/5", "value", "1/2/3/4/5")]
    public void OptionalParameterPlusCatchAllRoute(string template, string path, string parameterValue, string catchAllValue)
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute(template)
            .Build();
 
        var context = new RouteContext(path);
 
        // Act
        table.Route(context);
 
        // Assert
        Assert.True(context.Parameters.TryGetValue("parameter", out var parameter));
        Assert.True(context.Parameters.TryGetValue("catchAll", out var catchAll));
        Assert.Equal(parameterValue, parameter);
        Assert.Equal(catchAllValue, catchAll);
    }
 
    [Fact]
    public void CanMatchCatchAllParametersWithConstraints_NotMatchingRoute()
    {
        // Arrange
 
        // Routes are added in reverse precedence order
        var table = new TestRouteTableBuilder()
            .AddRoute("/values/{*values:int}")
            .Build();
 
        var context = new RouteContext("/values/1/2/3/4/5/A");
 
        // Act
        table.Route(context);
 
        // Assert
        Assert.Null(context.Handler);
    }
 
    [Fact]
    public void CanMatchOptionalParameterWithoutConstraints()
    {
        // Arrange
        var template = "/optional/{value?}";
        var contextUrl = "/optional/";
        string convertedValue = null;
 
        var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
        var context = new RouteContext(contextUrl);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        if (context.Handler == null)
        {
            // Make it easier to track down failing tests when using MemberData
            throw new InvalidOperationException($"Failed to match template '{template}'.");
        }
        Assert.Equal(new Dictionary<string, object>
            {
                { "value", convertedValue }
            }, context.Parameters);
    }
 
    public static IEnumerable<object[]> CanMatchOptionalParameterWithConstraintCases() => new object[][]
{
            new object[] { "/optional/{value:bool?}", "/optional/", null },
            new object[] { "/optional/{value:datetime?}", "/optional/", null },
            new object[] { "/optional/{value:decimal?}", "/optional/", null },
};
 
    [Theory]
    [MemberData(nameof(CanMatchOptionalParameterWithConstraintCases))]
    public void CanMatchOptionalParameterWithConstraint(string template, string contextUrl, object convertedValue)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
        var context = new RouteContext(contextUrl);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        if (context.Handler == null)
        {
            // Make it easier to track down failing tests when using MemberData
            throw new InvalidOperationException($"Failed to match template '{template}'.");
        }
        Assert.Equal(new Dictionary<string, object>
            {
                { "value", convertedValue }
            }, context.Parameters);
    }
 
    [Fact]
    public void CanMatchMultipleOptionalParameterWithConstraint()
    {
        // Arrange
        var template = "/optional/{value:datetime?}/{value2:datetime?}";
        var contextUrl = "/optional/";
        object convertedValue = null;
 
        var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
        var context = new RouteContext(contextUrl);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        if (context.Handler == null)
        {
            // Make it easier to track down failing tests when using MemberData
            throw new InvalidOperationException($"Failed to match template '{template}'.");
        }
        Assert.Equal(new Dictionary<string, object>
            {
                { "value", convertedValue },
                { "value2", convertedValue }
            }, context.Parameters);
    }
 
    public static IEnumerable<object[]> CanMatchSegmentWithMultipleConstraintsCases() => new object[][]
{
            new object[] { "/{value:double:int}/", "/15", 15 },
            new object[] { "/{value:double:int?}/", "/", null },
};
 
    [Theory]
    [MemberData(nameof(CanMatchSegmentWithMultipleConstraintsCases))]
    public void CanMatchSegmentWithMultipleConstraints(string template, string contextUrl, object convertedValue)
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder().AddRoute(template).Build();
        var context = new RouteContext(contextUrl);
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.Equal(new Dictionary<string, object>
            {
                { "value", convertedValue }
            }, context.Parameters);
    }
 
    [Fact]
    public void PrefersLiteralTemplateOverTemplateWithParameters()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/an/awesome/path", typeof(TestHandler1))
            .AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2))
            .Build();
        var context = new RouteContext("/an/awesome/path");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Null(context.Parameters);
    }
 
    [Fact]
    public void PrefersLiteralTemplateOverTemplateWithOptionalParameters()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/users/1", typeof(TestHandler1))
            .AddRoute("/users/{id?}", typeof(TestHandler2))
            .Build();
        var context = new RouteContext("/users/1");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Null(context.Parameters);
    }
 
    [Fact]
    public void ThrowsForOptionalParametersAndNonOptionalParameters()
    {
        // Arrange, act & assert
        Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
            .AddRoute("/users/{id}", typeof(TestHandler1))
            .AddRoute("/users/{id?}", typeof(TestHandler2))
            .Build());
    }
 
    [Theory]
    [InlineData("{*catchall}/literal")]
    [InlineData("{*catchall}/{parameter}")]
    [InlineData("{*catchall}/{parameter?}")]
    [InlineData("{*catchall}/{*other}")]
    [InlineData("prefix/{*catchall}/literal")]
    [InlineData("prefix/{*catchall}/{parameter}")]
    [InlineData("prefix/{*catchall}/{parameter?}")]
    [InlineData("prefix/{*catchall}/{*other}")]
    public void ThrowsWhenCatchAllIsNotTheLastSegment(string template)
    {
        // Arrange, act & assert
        Assert.Throws<RoutePatternException>(() => new TestRouteTableBuilder()
            .AddRoute(template)
            .Build());
    }
 
    [Theory(Skip = "This is allowed in ASP.NET Core routing.")]
    [InlineData("{optional?}/literal")]
    [InlineData("{optional?}/{parameter}")]
    [InlineData("{optional?}/{parameter:int}")]
    [InlineData("prefix/{optional?}/literal")]
    [InlineData("prefix/{optional?}/{parameter}")]
    [InlineData("prefix/{optional?}/{parameter:int}")]
    public void ThrowsForOptionalParametersFollowedByNonOptionalParameters(string template)
    {
        // Arrange, act & assert
        Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
            .AddRoute(template)
            .Build());
    }
 
    [Theory]
    [InlineData("{parameter}", "{parameter?}")]
    [InlineData("{parameter:int}", "{parameter:bool?}")]
    public void ThrowsForAmbiguousRoutes(string first, string second)
    {
        // Arrange, act & assert
        var exception = Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
            .AddRoute(first, typeof(TestHandler1))
            .AddRoute(second, typeof(TestHandler2))
            .Build());
 
        exception.Message.Contains("The following routes are ambiguous");
    }
 
    // It's important the precedence is inverted here to also validate that
    // the precedence is correct in these cases
    [Theory]
    [InlineData("{optional?}", "/")]
    [InlineData("{optional?}", "literal")]
    [InlineData("{optional?}", "{optional:int?}")]
    [InlineData("{*catchAll:int}", "{optional?}")]
    [InlineData("{*catchAll}", "{optional?}")]
    [InlineData("literal/{optional?}", "/")]
    [InlineData("literal/{optional?}", "literal")]
    [InlineData("literal/{optional?}", "literal/{optional:int?}")]
    [InlineData("literal/{*catchAll:int}", "literal/{optional?}")]
    [InlineData("literal/{*catchAll}", "literal/{optional?}")]
    [InlineData("{param}/{optional?}", "/")]
    [InlineData("{param}/{optional?}", "{param}")]
    [InlineData("{param}/{optional?}", "{param}/{optional:int?}")]
    [InlineData("{param}/{*catchAll:int}", "{param}/{optional?}")]
    [InlineData("{param}/{*catchAll}", "{param}/{optional?}")]
    [InlineData("{param1?}/{param2?}/{param3?}/{optional?}", "/")]
    [InlineData("{param1?}/{param2?}/{param3?}/{optional?}", "{param1?}/{param2?}/{param3?}/{optional:int?}")]
    [InlineData("{param1?}/{param2?}/{param3?}/{optional?}", "{param1?}/{param2?}/{param3:int?}/{optional?}")]
    [InlineData("{param1?}/{param2?}/{param3:int?}/{optional?}", "{param1?}/{param2?}")]
    [InlineData("{param1?}/{param2?}/{param3?}/{*catchAll:int}", "{param1?}/{param2?}/{param3?}/{optional?}")]
    [InlineData("{param1?}/{param2?}/{param3?}/{*catchAll}", "{param1?}/{param2?}/{param3?}/{optional?}")]
    public void DoesNotThrowForNonAmbiguousRoutes(string first, string second)
    {
        // Arrange
        var builder = new TestRouteTableBuilder()
            .AddRoute(first, typeof(TestHandler1))
            .AddRoute(second, typeof(TestHandler2));
 
        //var expectedOrder = new[] { second, first };
 
        // Act
        var table = builder.Build();
 
        // Assert
        //var tableTemplates = table .Routes.Select(p => p.Template.TemplateText).ToArray();
        //Assert.Equal(expectedOrder, tableTemplates);
    }
 
    [Fact]
    public void ThrowsForLiteralWithQuestionMark()
    {
        // Arrange, act & assert
        Assert.Throws<RoutePatternException>(() => new TestRouteTableBuilder()
            .AddRoute("literal?")
            .Build());
    }
 
    [Fact]
    public void PrefersLiteralTemplateOverParameterizedTemplates()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/users/1/friends", typeof(TestHandler1))
            .AddRoute("/users/{id}/{location}", typeof(TestHandler2))
            .AddRoute("/users/1/{location}", typeof(TestHandler2))
            .Build();
        var context = new RouteContext("/users/1/friends");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Equal(typeof(TestHandler1), context.Handler);
        Assert.Null(context.Parameters);
    }
 
    [Fact]
    public void PrefersShorterRoutesOverLongerRoutes()
    {
        // Arrange & Act
        var handler = typeof(int);
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/an/awesome/path")
            .AddRoute("/an/awesome/", handler).Build();
 
        // Act
        //Assert.Equal("an/awesome", routeTable.Routes[0].Template.TemplateText);
    }
 
    [Fact]
    public void PrefersMoreConstraintsOverFewer()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/products/{id}")
            .AddRoute("/products/{id:int}").Build();
        var context = new RouteContext("/products/456");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Equal(context.Parameters, new Dictionary<string, object>
            {
                { "id", 456 }
            });
    }
 
    [Fact]
    public void PrefersRoutesThatMatchMoreSegments()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/{anythingGoes}", typeof(TestHandler1))
            .AddRoute("/users/{id?}", typeof(TestHandler2))
            .Build();
        var context = new RouteContext("/users/1");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        Assert.NotNull(context.Handler);
        Assert.Equal(typeof(TestHandler2), context.Handler);
        Assert.NotNull(context.Parameters);
    }
 
    [Fact]
    public void ProducesAStableOrderForNonAmbiguousRoutes()
    {
        // Arrange & Act
        var handler = typeof(int);
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/an/awesome/", handler)
            .AddRoute("/a/brilliant/").Build();
 
        // Act
        //Assert.Equal("a/brilliant", routeTable.Routes[0].Template.TemplateText);
    }
 
    [Fact]
    public void DoesNotThrowIfStableSortComparesRouteWithItself()
    {
        // Test for https://github.com/dotnet/aspnetcore/issues/13313
        // Arrange & Act
        var builder = new TestRouteTableBuilder();
        builder.AddRoute("r16");
        builder.AddRoute("r05");
        builder.AddRoute("r09");
        builder.AddRoute("r00");
        builder.AddRoute("r13");
        builder.AddRoute("r02");
        builder.AddRoute("r03");
        builder.AddRoute("r10");
        builder.AddRoute("r15");
        builder.AddRoute("r14");
        builder.AddRoute("r12");
        builder.AddRoute("r07");
        builder.AddRoute("r11");
        builder.AddRoute("r08");
        builder.AddRoute("r06");
        builder.AddRoute("r04");
        builder.AddRoute("r01");
 
        // Act
        var routeTable = builder.Build();
 
        // Assert
        Assert.NotNull(routeTable);
        var matchingTree = Assert.Single(routeTable.TreeRouter.MatchingTrees);
 
        //Assert.Equal(17, routeTable.Routes.Length);
        //for (var i = 0; i < 17; i++)
        //{
        //    var templateText = "r" + i.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
        //    Assert.Equal(templateText, routeTable.Routes[i].Template.TemplateText);
        //}
    }
 
    [Theory]
    [InlineData("/literal", "/Literal/")]
    [InlineData("/{parameter}", "/{parameter}/")]
    [InlineData("/literal/{parameter}", "/Literal/{something}")]
    [InlineData("/{parameter}/literal/{something}", "{param}/Literal/{else}")]
    public void DetectsAmbiguousRoutes(string left, string right)
    {
        // Arrange
        var expectedMessage = $@"The following routes are ambiguous:
'{left.Trim('/')}' in '{typeof(object).FullName}'
'{right.Trim('/')}' in '{typeof(object).FullName}'
";
        // Act
        var exception = Assert.Throws<InvalidOperationException>(() => new TestRouteTableBuilder()
            .AddRoute(left)
            .AddRoute(right).Build());
 
        Assert.Equal(expectedMessage, exception.Message);
    }
 
    [Fact]
    public void SuppliesNullForUnusedHandlerParameters()
    {
        // Arrange
        var routeTable = new TestRouteTableBuilder()
            .AddRoute("/{unrelated}", typeof(TestHandler2))
            .AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1))
            .AddRoute("/products/{param1:int}", typeof(TestHandler1))
            .AddRoute("/", typeof(TestHandler1))
            .Build();
        var context = new RouteContext("/products/456");
 
        // Act
        routeTable.Route(context);
 
        // Assert
        //Assert.Collection(
        //    routeTable.Routes,
        //    route =>
        //    {
        //        Assert.Same(typeof(TestHandler1), route.Handler);
        //        Assert.Equal("/", route.Template.TemplateText);
        //        Assert.Equal(new[] { "PaRam1", "param2" }, route.UnusedRouteParameterNames.OrderBy(id => id).ToArray());
        //    },
        //    route =>
        //    {
        //        Assert.Same(typeof(TestHandler1), route.Handler);
        //        Assert.Equal("products/{param1:int}", route.Template.TemplateText);
        //        Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames.OrderBy(id => id).ToArray());
        //    },
        //    route =>
        //    {
        //        Assert.Same(typeof(TestHandler1), route.Handler);
        //        Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText);
        //        Assert.Null(route.UnusedRouteParameterNames);
        //    },
        //    route =>
        //    {
        //        Assert.Same(typeof(TestHandler2), route.Handler);
        //        Assert.Equal("{unrelated}", route.Template.TemplateText);
        //        Assert.Null(route.UnusedRouteParameterNames);
        //    });
 
        Assert.Same(typeof(TestHandler1), context.Handler);
        Assert.Equal(new Dictionary<string, object>
            {
                { "param1", 456 },
                { "param2", null },
            }, context.Parameters);
    }
 
    private class TestRouteTableBuilder
    {
        private readonly ServiceProvider _serviceProvider;
 
        public TestRouteTableBuilder()
        {
            _serviceProvider = new ServiceCollection()
                .AddLogging()
                .BuildServiceProvider();
        }
        readonly IList<(string Template, Type Handler)> _routeTemplates = new List<(string, Type)>();
        readonly Type _handler = typeof(object);
 
        public TestRouteTableBuilder AddRoute(string template, Type handler = null)
        {
            _routeTemplates.Add((template, handler ?? _handler));
            return this;
        }
 
        public RouteTable Build()
        {
            try
            {
                var templatesByHandler = _routeTemplates
                    .GroupBy(rt => rt.Handler)
                    .ToDictionary(group => group.Key, group => group.Select(g => g.Template).ToArray());
                return RouteTableFactory.Create(templatesByHandler, _serviceProvider);
            }
            catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
            {
                // ToArray() will wrap our exception in its own.
                throw ex.InnerException;
            }
        }
    }
 
    class TestHandler1 { }
    class TestHandler2 { }
 
    [Route("/ComponentWithoutExcludeFromInteractiveRoutingAttribute")]
    public class ComponentWithoutExcludeFromInteractiveRoutingAttribute : ComponentBase { }
 
    [Route("/ComponentWithExcludeFromInteractiveRoutingAttribute")]
    [ExcludeFromInteractiveRouting]
    public class ComponentWithExcludeFromInteractiveRoutingAttribute : ComponentBase { }
}