File: Patterns\RoutePatternFactoryTest.cs
Web Access
Project: src\src\Http\Routing\test\UnitTests\Microsoft.AspNetCore.Routing.Tests.csproj (Microsoft.AspNetCore.Routing.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Template;
using Moq;
 
namespace Microsoft.AspNetCore.Routing.Patterns;
 
public class RoutePatternFactoryTest
{
    [Fact]
    public void Pattern_MergesDefaultValues()
    {
        // Arrange
        var template = "{a}/{b}/{c=19}";
        var defaults = new { a = "15", b = 17 };
        var constraints = new { };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Equal("15", actual.GetParameter("a").Default);
        Assert.Equal(17, actual.GetParameter("b").Default);
        Assert.Equal("19", actual.GetParameter("c").Default);
 
        Assert.Collection(
            actual.Defaults.OrderBy(kvp => kvp.Key),
            kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("15", kvp.Value); },
            kvp => { Assert.Equal("b", kvp.Key); Assert.Equal(17, kvp.Value); },
            kvp => { Assert.Equal("c", kvp.Key); Assert.Equal("19", kvp.Value); });
    }
 
    [Fact]
    public void Pattern_ExtraDefaultValues()
    {
        // Arrange
        var template = "{a}/{b}/{c}";
        var defaults = new { d = "15", e = 17 };
        var constraints = new { };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.Defaults.OrderBy(kvp => kvp.Key),
            kvp => { Assert.Equal("d", kvp.Key); Assert.Equal("15", kvp.Value); },
            kvp => { Assert.Equal("e", kvp.Key); Assert.Equal(17, kvp.Value); });
    }
 
    [Fact]
    public void Pattern_DifferentDuplicateDefaultValue_Throws()
    {
        // Arrange
        var template = "{a=13}/{b}/{c}";
        var defaults = new { a = "15", };
        var constraints = new { };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments));
 
        // Assert
        Assert.Equal(
            "The route parameter 'a' has both an inline default value and an explicit default " +
            "value specified. A route parameter cannot contain an inline default value when a " +
            "default value is specified explicitly. Consider removing one of them.",
            ex.Message);
    }
 
    [Fact]
    public void Pattern_SameDuplicateDefaultValue()
    {
        // Arrange
        var template = "{a=13}/{b}/{c}";
        var defaults = new { a = "13", };
        var constraints = new { };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.Defaults,
            kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("13", kvp.Value); });
    }
 
    [Fact]
    public void Pattern_OptionalParameterDefaultValue_Throws()
    {
        // Arrange
        var template = "{a}/{b}/{c?}";
        var defaults = new { c = "15", };
        var constraints = new { };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments));
 
        // Assert
        Assert.Equal(
            "An optional parameter cannot have default value.",
            ex.Message);
    }
 
    [Fact]
    public void Pattern_MergesConstraints()
    {
        // Arrange
        var template = "{a:int}/{b}/{c}";
        var defaults = new { };
        var constraints = new { a = new RegexRouteConstraint("foo"), b = new RegexRouteConstraint("bar") };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.GetParameter("a").ParameterPolicies,
            c => Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy),
            c => Assert.Equal("int", c.Content));
        Assert.Collection(
            actual.GetParameter("b").ParameterPolicies,
            c => Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy));
 
        Assert.Collection(
            actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("a", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy),
                    c => Assert.Equal("int", c.Content));
            },
            kvp =>
            {
                Assert.Equal("b", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy));
            });
    }
 
    [Fact]
    public void Pattern_ExtraConstraints()
    {
        // Arrange
        var template = "{a}/{b}/{c}";
        var defaults = new { };
        var constraints = new { d = new RegexRouteConstraint("foo"), e = new RegexRouteConstraint("bar") };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("d", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy));
            },
            kvp =>
            {
                Assert.Equal("e", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy));
            });
    }
 
    [Fact]
    public void Pattern_ExtraConstraints_MultipleConstraintsForKey()
    {
        // Arrange
        var template = "{a}/{b}/{c}";
        var defaults = new { };
        var constraints = new { d = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("d", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.Equal("foo", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()),
                    c => Assert.Equal("bar", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()),
                    c => Assert.Equal("^(baz)$", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()));
            });
    }
 
    [Fact]
    public void Pattern_ExtraConstraints_MergeMultipleConstraintsForKey()
    {
        // Arrange
        var template = "{a:int}/{b}/{c:int}";
        var defaults = new { };
        var constraints = new { b = "fizz", c = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("a", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.Equal("int", c.Content));
            },
            kvp =>
            {
                Assert.Equal("b", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.Equal("^(fizz)$", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()));
            },
            kvp =>
            {
                Assert.Equal("c", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.Equal("foo", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()),
                    c => Assert.Equal("bar", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()),
                    c => Assert.Equal("^(baz)$", Assert.IsType<RegexRouteConstraint>(c.ParameterPolicy).Constraint.ToString()),
                    c => Assert.Equal("int", c.Content));
            });
    }
 
    [Fact]
    public void Pattern_ExtraConstraints_NestedArray_Throws()
    {
        // Arrange
        var template = "{a}/{b}/{c:int}";
        var defaults = new { };
        var constraints = new { c = new object[] { new object[0] } };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act & Assert
        Assert.Throws<InvalidOperationException>(() =>
        {
            RoutePatternFactory.Pattern(
                original.RawText,
                defaults,
                constraints,
                original.PathSegments);
        });
    }
 
    [Fact]
    public void Pattern_ExtraConstraints_RouteConstraint()
    {
        // Arrange
        var template = "{a}/{b}/{c}";
        var defaults = new { };
        var constraints = new { d = Mock.Of<IRouteConstraint>(), e = Mock.Of<IRouteConstraint>(), };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("d", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.NotNull(c.ParameterPolicy));
            },
            kvp =>
            {
                Assert.Equal("e", kvp.Key);
                Assert.Collection(
                    kvp.Value,
                    c => Assert.NotNull(c.ParameterPolicy));
            });
    }
 
    [Fact]
    public void Pattern_CreatesConstraintFromString()
    {
        // Arrange
        var template = "{a}/{b}/{c}";
        var defaults = new { };
        var constraints = new { d = "foo", };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var actual = RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments);
 
        // Assert
        Assert.Collection(
            actual.ParameterPolicies.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("d", kvp.Key);
                var regex = Assert.IsType<RegexRouteConstraint>(Assert.Single(kvp.Value).ParameterPolicy);
                Assert.Equal("^(foo)$", regex.Constraint.ToString());
            });
    }
 
    [Fact]
    public void Pattern_InvalidConstraintTypeThrows()
    {
        // Arrange
        var template = "{a}/{b}/{c}";
        var defaults = new { };
        var constraints = new { d = 17, };
 
        var original = RoutePatternFactory.Parse(template);
 
        // Act
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
            original.RawText,
            defaults,
            constraints,
            original.PathSegments));
 
        // Assert
        Assert.Equal(
            $"Invalid constraint '17'. A constraint must be of type 'string' or '{typeof(IRouteConstraint)}'.",
            ex.Message);
    }
 
    [Fact]
    public void Pattern_ArrayOfSegments_ShouldMakeCopyOfArrayOfSegments()
    {
        // Arrange
        var literalPartA = RoutePatternFactory.LiteralPart("A");
        var paramPartB = RoutePatternFactory.ParameterPart("B");
        var paramPartC = RoutePatternFactory.ParameterPart("C");
        var paramPartD = RoutePatternFactory.ParameterPart("D");
        var segments = new[]
            {
                    RoutePatternFactory.Segment(literalPartA, paramPartB),
                    RoutePatternFactory.Segment(paramPartC, literalPartA),
                    RoutePatternFactory.Segment(paramPartD),
                    RoutePatternFactory.Segment(literalPartA)
                };
 
        // Act
        var actual = RoutePatternFactory.Pattern(segments);
        segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E"));
        Array.Resize(ref segments, 2);
 
        // Assert
        Assert.Equal(3, actual.Parameters.Count);
        Assert.Same(paramPartB, actual.Parameters[0]);
        Assert.Same(paramPartC, actual.Parameters[1]);
        Assert.Same(paramPartD, actual.Parameters[2]);
    }
 
    [Fact]
    public void Pattern_RawTextAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments()
    {
        // Arrange
        var rawText = "raw";
        var literalPartA = RoutePatternFactory.LiteralPart("A");
        var paramPartB = RoutePatternFactory.ParameterPart("B");
        var paramPartC = RoutePatternFactory.ParameterPart("C");
        var paramPartD = RoutePatternFactory.ParameterPart("D");
        var segments = new[]
            {
                    RoutePatternFactory.Segment(literalPartA, paramPartB),
                    RoutePatternFactory.Segment(paramPartC, literalPartA),
                    RoutePatternFactory.Segment(paramPartD),
                    RoutePatternFactory.Segment(literalPartA)
                };
 
        // Act
        var actual = RoutePatternFactory.Pattern(rawText, segments);
        segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E"));
        Array.Resize(ref segments, 2);
 
        // Assert
        Assert.Equal(3, actual.Parameters.Count);
        Assert.Same(paramPartB, actual.Parameters[0]);
        Assert.Same(paramPartC, actual.Parameters[1]);
        Assert.Same(paramPartD, actual.Parameters[2]);
    }
 
    [Fact]
    public void Pattern_DefaultsAndParameterPoliciesAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments()
    {
        // Arrange
        object defaults = new { B = 12, C = 4 };
        object parameterPolicies = null;
        var literalPartA = RoutePatternFactory.LiteralPart("A");
        var paramPartB = RoutePatternFactory.ParameterPart("B");
        var paramPartC = RoutePatternFactory.ParameterPart("C");
        var paramPartD = RoutePatternFactory.ParameterPart("D");
        var segments = new[]
            {
                    RoutePatternFactory.Segment(literalPartA, paramPartB),
                    RoutePatternFactory.Segment(paramPartC, literalPartA),
                    RoutePatternFactory.Segment(paramPartD),
                    RoutePatternFactory.Segment(literalPartA)
                };
 
        // Act
        var actual = RoutePatternFactory.Pattern(defaults, parameterPolicies, segments);
        segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E"));
        Array.Resize(ref segments, 2);
 
        // Assert
        Assert.Equal(3, actual.Parameters.Count);
        Assert.Equal(paramPartB.Name, actual.Parameters[0].Name);
        Assert.Equal(12, actual.Parameters[0].Default);
        Assert.Null(paramPartB.Default);
        Assert.NotSame(paramPartB, actual.Parameters[0]);
        Assert.Equal(paramPartC.Name, actual.Parameters[1].Name);
        Assert.Equal(4, actual.Parameters[1].Default);
        Assert.NotSame(paramPartC, actual.Parameters[1]);
        Assert.Null(paramPartC.Default);
        Assert.Equal(paramPartD.Name, actual.Parameters[2].Name);
        Assert.Null(actual.Parameters[2].Default);
        Assert.Same(paramPartD, actual.Parameters[2]);
        Assert.Null(paramPartD.Default);
    }
 
    [Fact]
    public void Pattern_RawTextAndDefaultsAndParameterPoliciesAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments()
    {
        // Arrange
        var rawText = "raw";
        object defaults = new { B = 12, C = 4 };
        object parameterPolicies = null;
        var literalPartA = RoutePatternFactory.LiteralPart("A");
        var paramPartB = RoutePatternFactory.ParameterPart("B");
        var paramPartC = RoutePatternFactory.ParameterPart("C");
        var paramPartD = RoutePatternFactory.ParameterPart("D");
        var segments = new[]
            {
                    RoutePatternFactory.Segment(literalPartA, paramPartB),
                    RoutePatternFactory.Segment(paramPartC, literalPartA),
                    RoutePatternFactory.Segment(paramPartD),
                    RoutePatternFactory.Segment(literalPartA)
                };
 
        // Act
        var actual = RoutePatternFactory.Pattern(rawText, defaults, parameterPolicies, segments);
        segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E"));
        Array.Resize(ref segments, 2);
 
        // Assert
        Assert.Equal(3, actual.Parameters.Count);
        Assert.Equal(paramPartB.Name, actual.Parameters[0].Name);
        Assert.Equal(12, actual.Parameters[0].Default);
        Assert.Null(paramPartB.Default);
        Assert.NotSame(paramPartB, actual.Parameters[0]);
        Assert.Equal(paramPartC.Name, actual.Parameters[1].Name);
        Assert.Equal(4, actual.Parameters[1].Default);
        Assert.NotSame(paramPartC, actual.Parameters[1]);
        Assert.Null(paramPartC.Default);
        Assert.Equal(paramPartD.Name, actual.Parameters[2].Name);
        Assert.Null(actual.Parameters[2].Default);
        Assert.Same(paramPartD, actual.Parameters[2]);
        Assert.Null(paramPartD.Default);
    }
 
    [Fact]
    public void Parse_WithRequiredValues()
    {
        // Arrange
        var template = "{controller=Home}/{action=Index}/{id?}";
        var defaults = new { area = "Admin", };
        var policies = new { };
        var requiredValues = new { area = "Admin", controller = "Store", action = "Index", };
 
        // Act
        var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
 
        // Assert
        Assert.Collection(
            action.RequiredValues.OrderBy(kvp => kvp.Key),
            kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
            kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("Admin", kvp.Value); },
            kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
    }
 
    [Fact]
    public void Parse_WithRequiredValues_AllowsNullRequiredValue()
    {
        // Arrange
        var template = "{controller=Home}/{action=Index}/{id?}";
        var defaults = new { };
        var policies = new { };
        var requiredValues = new { area = (string)null, controller = "Store", action = "Index", };
 
        // Act
        var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
 
        // Assert
        Assert.Collection(
            action.RequiredValues.OrderBy(kvp => kvp.Key),
            kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
            kvp => { Assert.Equal("area", kvp.Key); Assert.Null(kvp.Value); },
            kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
    }
 
    [Fact]
    public void Parse_WithRequiredValues_AllowsEmptyRequiredValue()
    {
        // Arrange
        var template = "{controller=Home}/{action=Index}/{id?}";
        var defaults = new { };
        var policies = new { };
        var requiredValues = new { area = "", controller = "Store", action = "Index", };
 
        // Act
        var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
 
        // Assert
        Assert.Collection(
            action.RequiredValues.OrderBy(kvp => kvp.Key),
            kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
            kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("", kvp.Value); },
            kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
    }
 
    [Fact]
    public void Parse_WithRequiredValues_ThrowsForNonParameterNonDefault()
    {
        // Arrange
        var template = "{controller=Home}/{action=Index}/{id?}";
        var defaults = new { };
        var policies = new { };
        var requiredValues = new { area = "Admin", controller = "Store", action = "Index", };
 
        // Act
        var exception = Assert.Throws<InvalidOperationException>(() =>
        {
            var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
        });
 
        // Assert
        Assert.Equal(
            "No corresponding parameter or default value could be found for the required value " +
            "'area=Admin'. A non-null required value must correspond to a route parameter or the " +
            "route pattern must have a matching default value.",
            exception.Message);
    }
 
    [Fact]
    public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies()
    {
        // Arrange (going through hoops to get an array of RoutePatternParameterPolicyReference)
        const string name = "Id";
        var defaults = new { a = "13", };
        var x = new InlineConstraint("x");
        var y = new InlineConstraint("y");
        var z = new InlineConstraint("z");
        var constraints = new[] { x, y, z };
        var templatePart = TemplatePart.CreateParameter("t", false, false, null, constraints);
        var routePatternParameterPart = (RoutePatternParameterPart)templatePart.ToRoutePatternPart();
        var policies = routePatternParameterPart.ParameterPolicies.ToArray();
 
        // Act
        var parameterPart = RoutePatternFactory.ParameterPart(name, defaults, RoutePatternParameterKind.Standard, policies);
        policies[0] = null;
        Array.Resize(ref policies, 2);
 
        // Assert
        Assert.NotNull(parameterPart.ParameterPolicies);
        Assert.Equal(3, parameterPart.ParameterPolicies.Count);
        Assert.NotNull(parameterPart.ParameterPolicies[0]);
        Assert.NotNull(parameterPart.ParameterPolicies[1]);
        Assert.NotNull(parameterPart.ParameterPolicies[2]);
    }
 
    [Fact]
    public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndEnumerableOfParameterPolicies_ShouldMakeCopyOfParameterPolicies()
    {
        // Arrange (going through hoops to get an enumerable of RoutePatternParameterPolicyReference)
        const string name = "Id";
        var defaults = new { a = "13", };
        var x = new InlineConstraint("x");
        var y = new InlineConstraint("y");
        var z = new InlineConstraint("z");
        var constraints = new[] { x, y, z };
        var templatePart = TemplatePart.CreateParameter("t", false, false, null, constraints);
        var routePatternParameterPart = (RoutePatternParameterPart)templatePart.ToRoutePatternPart();
        var policies = routePatternParameterPart.ParameterPolicies.ToList();
 
        // Act
        var parameterPart = RoutePatternFactory.ParameterPart(name, defaults, RoutePatternParameterKind.Standard, policies);
        policies[0] = null;
        policies.RemoveAt(1);
 
        // Assert
        Assert.NotNull(parameterPart.ParameterPolicies);
        Assert.Equal(3, parameterPart.ParameterPolicies.Count);
        Assert.NotNull(parameterPart.ParameterPolicies[0]);
        Assert.NotNull(parameterPart.ParameterPolicies[1]);
        Assert.NotNull(parameterPart.ParameterPolicies[2]);
    }
 
    [Fact]
    public void Segment_EnumerableOfParts()
    {
        // Arrange
        var paramPartB = RoutePatternFactory.ParameterPart("B");
        var paramPartC = RoutePatternFactory.ParameterPart("C");
        var paramPartD = RoutePatternFactory.ParameterPart("D");
        var parts = new[] { paramPartB, paramPartC, paramPartD };
 
        // Act
        var actual = RoutePatternFactory.Segment((IEnumerable<RoutePatternParameterPart>)parts);
        parts[1] = RoutePatternFactory.ParameterPart("E");
        Array.Resize(ref parts, 2);
 
        // Assert
        Assert.Equal(3, actual.Parts.Count);
        Assert.Same(paramPartB, actual.Parts[0]);
        Assert.Same(paramPartC, actual.Parts[1]);
        Assert.Same(paramPartD, actual.Parts[2]);
    }
 
    [Fact]
    public void Segment_ArrayOfParts()
    {
        // Arrange
        var paramPartB = RoutePatternFactory.ParameterPart("B");
        var paramPartC = RoutePatternFactory.ParameterPart("C");
        var paramPartD = RoutePatternFactory.ParameterPart("D");
        var parts = new[] { paramPartB, paramPartC, paramPartD };
 
        // Act
        var actual = RoutePatternFactory.Segment(parts);
        parts[1] = RoutePatternFactory.ParameterPart("E");
        Array.Resize(ref parts, 2);
 
        // Assert
        Assert.Equal(3, actual.Parts.Count);
        Assert.Same(paramPartB, actual.Parts[0]);
        Assert.Same(paramPartC, actual.Parts[1]);
        Assert.Same(paramPartD, actual.Parts[2]);
    }
 
    [Theory]
    [InlineData("/a/b", "")]
    [InlineData("/a/b", "/")]
    [InlineData("/a/b/", "")]
    [InlineData("/a/b/", "/")]
    [InlineData("/a", "b/")]
    [InlineData("/a/", "b/")]
    [InlineData("/a", "/b/")]
    [InlineData("/a/", "/b/")]
    [InlineData("/", "a/b/")]
    [InlineData("/", "/a/b/")]
    [InlineData("", "a/b/")]
    [InlineData("", "/a/b/")]
    public void Combine_HandlesEmptyPatternsAndDuplicateSeperatorsInRawText(string leftTemplate, string rightTemplate)
    {
        var left = RoutePatternFactory.Parse(leftTemplate);
        var right = RoutePatternFactory.Parse(rightTemplate);
 
        var combined = RoutePatternFactory.Combine(left, right);
 
        static Action<RoutePatternPathSegment> AssertLiteral(string literal)
        {
            return segment =>
            {
                var part = Assert.IsType<RoutePatternLiteralPart>(Assert.Single(segment.Parts));
                Assert.Equal(literal, part.Content);
            };
        }
 
        Assert.Equal("/a/b/", combined.RawText);
        Assert.Collection(combined.PathSegments, AssertLiteral("a"), AssertLiteral("b"));
    }
 
    [Fact]
    public void Combine_WithDuplicateParameters_Throws()
    {
        // Parameter names are case insensitive
        var left = RoutePatternFactory.Parse("/{id}");
        var right = RoutePatternFactory.Parse("/{ID}");
 
        var ex = Assert.Throws<RoutePatternException>(() => RoutePatternFactory.Combine(left, right));
 
        Assert.Equal("/{id}/{ID}", ex.Pattern);
        Assert.Equal("The route parameter name 'ID' appears more than one time in the route template.", ex.Message);
    }
 
    [Fact]
    public void Combine_WithDuplicateDefaults_DoesNotThrow()
    {
        // Keys should be case insensitive.
        var left = RoutePatternFactory.Parse("/a/{x=foo}");
        var right = RoutePatternFactory.Parse("/b", defaults: new { X = "foo" }, parameterPolicies: null);
 
        var combined = RoutePatternFactory.Combine(left, right);
 
        Assert.Equal("/a/{x=foo}/b", combined.RawText);
 
        var (key, value) = Assert.Single(combined.Defaults);
        Assert.Equal("x", key);
        Assert.Equal("foo", value);
    }
 
    [Fact]
    public void Combine_WithDuplicateRequiredValues_DoesNotThrow()
    {
        // Keys should be case insensitive.
        // The required value must be a parameter or a default to parse. Since we cannot repeat parameters, we set defaults instead.
        var left = RoutePatternFactory.Parse("/a", defaults: new { x = "foo" }, parameterPolicies: null, requiredValues: new { x = "foo" });
        var right = RoutePatternFactory.Parse("/b", defaults: new { X = "foo" }, parameterPolicies: null, requiredValues: new { X = "foo" });
 
        var combined = RoutePatternFactory.Combine(left, right);
 
        Assert.Equal("/a/b", combined.RawText);
 
        var (key, value) = Assert.Single(combined.RequiredValues);
        Assert.Equal("x", key);
        Assert.Equal("foo", value);
    }
 
    [Fact]
    public void Combine_WithDuplicateParameterPolicies_Throws()
    {
        // Since even the exact same instance and keys throw, we don't bother testing different key casing.
        var policies = new { X = new RegexRouteConstraint("x"), };
 
        var left = RoutePatternFactory.Parse("/a", defaults: null, parameterPolicies: policies);
        var right = RoutePatternFactory.Parse("/b", defaults: null, parameterPolicies: policies);
 
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Combine(left, right));
        Assert.Equal("MapGroup cannot build a pattern for '/a/b' because the 'RoutePattern.ParameterPolicies' dictionary key 'X' has multiple values.", ex.Message);
    }
 
    [Fact]
    public void Combine_WithConflictingDefaults_Throws()
    {
        // Keys should be case insensitive but not values.
        // Value differs in casing. As long as object.Equals(object?, object?) returns false, there's a conflict.
        var left = RoutePatternFactory.Parse("/a/{x=foo}");
        var right = RoutePatternFactory.Parse("/b", defaults: new { X = "Foo" }, parameterPolicies: null);
 
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Combine(left, right));
        Assert.Equal("MapGroup cannot build a pattern for '/a/{x=foo}/b' because the 'RoutePattern.Defaults' dictionary key 'X' has multiple values.", ex.Message);
    }
 
    [Fact]
    public void Combine_WithConflictingRequiredValues_Throws()
    {
        // Keys should be case insensitive but not values.
        // Value differs in casing. As long as object.Equals(object?, object?) returns false, there's a conflict.
        // The required value must be a parameter or a default to parse. Since we cannot repeat parameters, we set defaults instead.
        var left = RoutePatternFactory.Parse("/a", defaults: new { x = "foo" }, parameterPolicies: null, requiredValues: new { x = "foo" });
        var right = RoutePatternFactory.Parse("/b", defaults: new { X = "foo" }, parameterPolicies: null, requiredValues: new { X = "Foo" });
 
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Combine(left, right));
        Assert.Equal("MapGroup cannot build a pattern for '/a/b' because the 'RoutePattern.RequiredValues' dictionary key 'X' has multiple values.", ex.Message);
    }
 
    [Fact]
    public void Combine_WithConflictingParameterPolicies_Throws()
    {
        // Even the exact same policy instance throws, but this verifies theres a conflict even if the policy is defined via a pattern in one part.
        // The policy is defined via a pattern in bot parts because parameters cannot be repeated.
        var left = RoutePatternFactory.Parse("/a/{x:string}");
        var right = RoutePatternFactory.Parse("/b", defaults: null, parameterPolicies: new { X = new RegexRouteConstraint("foo") });
 
        var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Combine(left, right));
        Assert.Equal("MapGroup cannot build a pattern for '/a/{x:string}/b' because the 'RoutePattern.ParameterPolicies' dictionary key 'X' has multiple values.", ex.Message);
    }
}