File: Routing\AttributeRouteTest.cs
Web Access
Project: src\src\Mvc\Mvc.Core\test\Microsoft.AspNetCore.Mvc.Core.Test.csproj (Microsoft.AspNetCore.Mvc.Core.Test)
// 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.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.Routing;
 
public class AttributeRouteTest
{
    private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
 
    // This test verifies that AttributeRoute can respond to changes in the AD collection. It does this
    // by running a successful request, then removing that action and verifying the next route isn't
    // successful.
    [Fact]
    public async Task AttributeRoute_UsesUpdatedActionDescriptors()
    {
        // Arrange
        ActionDescriptor selected = null;
 
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{key1}"
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Store/Buy/{key2}"
                    },
                },
            };
 
        Func<ActionDescriptor[], IRouter> handlerFactory = (_) =>
        {
            var handler = new Mock<IRouter>();
            handler
                .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
                .Returns<RouteContext>(routeContext =>
                {
                    if (routeContext.RouteData.Values.ContainsKey("key1"))
                    {
                        selected = actions[0];
                    }
                    else if (routeContext.RouteData.Values.ContainsKey("key2"))
                    {
                        selected = actions[1];
                    }
 
                    routeContext.Handler = (c) => Task.CompletedTask;
 
                    return Task.CompletedTask;
 
                });
            return handler.Object;
        };
 
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(handlerFactory, actionDescriptorProvider.Object);
 
        var requestServices = new Mock<IServiceProvider>(MockBehavior.Strict);
        requestServices
            .Setup(s => s.GetService(typeof(ILoggerFactory)))
            .Returns(NullLoggerFactory.Instance);
 
        var httpContext = new DefaultHttpContext();
        httpContext.Request.Path = new PathString("/api/Store/Buy/5");
        httpContext.RequestServices = requestServices.Object;
 
        var context = new RouteContext(httpContext);
 
        // Act 1
        await route.RouteAsync(context);
 
        // Assert 1
        Assert.NotNull(context.Handler);
        Assert.Equal("5", context.RouteData.Values["key2"]);
        Assert.Same(actions[1], selected);
 
        // Arrange 2 - remove the action and update the collection
        selected = null;
        actions.RemoveAt(1);
        actionDescriptorProvider
            .SetupGet(ad => ad.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(actions, version: 2));
 
        context = new RouteContext(httpContext);
 
        // Act 2
        await route.RouteAsync(context);
 
        // Assert 2
        Assert.Null(context.Handler);
        Assert.Empty(context.RouteData.Values);
        Assert.Null(selected);
    }
 
    [Fact]
    public void AttributeRoute_GetEntries_CreatesOutboundEntry()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.OutboundEntries,
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Empty(e.Defaults);
                Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal(17, e.Order);
                Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
            });
    }
 
    [Fact]
    public void AttributeRoute_GetEntries_CreatesOutboundEntry_WithConstraint()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id:int}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.OutboundEntries,
            e =>
            {
                Assert.Single(e.Constraints, kvp => kvp.Key == "id");
                Assert.Empty(e.Defaults);
                Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal(17, e.Order);
                Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
            });
    }
 
    [Fact]
    public void AttributeRoute_GetEntries_CreatesOutboundEntry_WithDefault()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{*slug=hello}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.OutboundEntries,
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Equal(new RouteValueDictionary(new { slug = "hello" }), e.Defaults);
                Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal(17, e.Order);
                Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
            });
    }
 
    // These actions seem like duplicates, but this is a real case that can happen where two different
    // actions define the same route info. Link generation happens based on the action name + controller
    // name.
    [Fact]
    public void AttributeRoute_GetEntries_CreatesOutboundEntry_ForEachAction()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index2" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.OutboundEntries,
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Empty(e.Defaults);
                Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal(17, e.Order);
                Assert.Equal(ToRouteValueDictionary(actions[0].RouteValues), e.RequiredLinkValues);
                Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
            },
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Empty(e.Defaults);
                Assert.Equal(RoutePrecedence.ComputeOutbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal(17, e.Order);
                Assert.Equal(ToRouteValueDictionary(actions[1].RouteValues), e.RequiredLinkValues);
                Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
            });
    }
 
    [Fact]
    public void AttributeRoute_GetEntries_CreatesInboundEntry()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.InboundEntries,
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Equal(17, e.Order);
                Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                Assert.Empty(e.Defaults);
            });
    }
 
    [Fact]
    public void AttributeRoute_GetEntries_CreatesInboundEntry_WithConstraint()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id:int}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.InboundEntries,
            e =>
            {
                Assert.Single(e.Constraints, kvp => kvp.Key == "id");
                Assert.Equal(17, e.Order);
                Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal("api/Blog/{id:int}", e.RouteTemplate.TemplateText);
                Assert.Empty(e.Defaults);
            });
    }
 
    [Fact]
    public void AttributeRoute_GetEntries_CreatesInboundEntry_WithDefault()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{*slug=hello}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.InboundEntries,
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Equal(17, e.Order);
                Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal("api/Blog/{*slug=hello}", e.RouteTemplate.TemplateText);
                Assert.Collection(
                    e.Defaults.OrderBy(kvp => kvp.Key),
                    kvp => Assert.Equal(new KeyValuePair<string, object>("slug", "hello"), kvp));
            });
    }
 
    // These actions seem like duplicates, but this is a real case that can happen where two different
    // actions define the same route info. Link generation happens based on the action name + controller
    // name.
    [Fact]
    public void AttributeRoute_GetEntries_CreatesInboundEntry_CombinesLikeActions()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index" },
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "api/Blog/{id}",
                        Name = "BLOG_INDEX",
                        Order = 17,
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "controller", "Blog" },
                        { "action", "Index2" },
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.InboundEntries,
            e =>
            {
                Assert.Empty(e.Constraints);
                Assert.Equal(17, e.Order);
                Assert.Equal(RoutePrecedence.ComputeInbound(e.RouteTemplate), e.Precedence);
                Assert.Equal("BLOG_INDEX", e.RouteName);
                Assert.Equal("api/Blog/{id}", e.RouteTemplate.TemplateText);
                Assert.Empty(e.Defaults);
            });
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("GetBlogById")]
    public void AttributeRoute_ThrowsRouteCreationException_ForConstraintsNotTakingArguments(string routeName)
    {
        // Arrange
        var routeTemplate = "api/Blog/{id:int(10)}";
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = routeTemplate,
                        Name = routeName
                    }
                },
            };
        var expectedErrorMessage = "An error occurred while adding a route to the route builder. " +
                    $"Route name '{routeName}' and template '{routeTemplate}'.";
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act & Assert
        var exception = Assert.Throws<RouteCreationException>(() =>
        {
            route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
        });
        Assert.Equal(expectedErrorMessage, exception.Message);
        Assert.IsType<RouteCreationException>(exception.InnerException);
    }
 
    [Fact]
    public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressLinkGeneration = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/{snake-cased-name}",
                        Name = "BLOG_INDEX2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressPathMatching = true,
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.OutboundEntries,
            e =>
            {
                Assert.Equal("BLOG_INDEX2", e.RouteName);
                Assert.Equal("blog/{snake-cased-name}", e.RouteTemplate.TemplateText);
            },
            e =>
            {
                Assert.Equal("BLOG_HOME", e.RouteName);
                Assert.Equal("blog/", e.RouteTemplate.TemplateText);
            });
    }
 
    [Fact]
    public void GetEntries_DoesNotCreateOutboundEntriesForAttributesWithSuppressForLinkGenerationSetToTrue_WhenMultipleAttributesHaveTheSameTemplate()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressLinkGeneration = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressPathMatching = true,
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.OutboundEntries,
            e =>
            {
                Assert.Equal("BLOG_LINK2", e.RouteName);
                Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText);
            },
            e =>
            {
                Assert.Equal("BLOG_HOME", e.RouteName);
                Assert.Equal("blog/", e.RouteTemplate.TemplateText);
            });
    }
 
    [Fact]
    public void GetEntries_DoesNotCreateInboundEntriesForAttributesWithSuppressForPathMatchingSetToTrue()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressLinkGeneration = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/{snake-cased-name}",
                        Name = "BLOG_LINK2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressPathMatching = true,
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.InboundEntries,
            e =>
            {
                Assert.Equal("BLOG_LINK1", e.RouteName);
                Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText);
            },
            e =>
            {
                Assert.Equal("BLOG_LINK2", e.RouteName);
                Assert.Equal("blog/{snake-cased-name}", e.RouteTemplate.TemplateText);
            });
    }
 
    [Fact]
    public void GetEntries_DoesNotCreateInboundEntriesForAttributesWithSuppressForPathMatchingSetToTrue_WhenMultipleAttributesHaveTheSameTemplate()
    {
        // Arrange
        var actions = new List<ActionDescriptor>()
            {
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK1",
                        SuppressPathMatching = true,
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/get/{id}",
                        Name = "BLOG_LINK2",
                    },
                },
                new ActionDescriptor()
                {
                    AttributeRouteInfo = new AttributeRouteInfo
                    {
                        Template = "blog/",
                        Name = "BLOG_HOME",
                        SuppressLinkGeneration = true,
                    },
                },
            };
 
        var builder = CreateBuilder();
        var actionDescriptorProvider = CreateActionDescriptorProvider(actions);
        var route = CreateRoute(CreateHandler().Object, actionDescriptorProvider.Object);
 
        // Act
        route.AddEntries(builder, actionDescriptorProvider.Object.ActionDescriptors);
 
        // Assert
        Assert.Collection(
            builder.InboundEntries,
            e =>
            {
                Assert.Equal("BLOG_LINK2", e.RouteName);
                Assert.Equal("blog/get/{id}", e.RouteTemplate.TemplateText);
            },
            e =>
            {
                Assert.Equal("BLOG_HOME", e.RouteName);
                Assert.Equal("blog/", e.RouteTemplate.TemplateText);
            });
    }
 
    private static TreeRouteBuilder CreateBuilder()
    {
        var services = new ServiceCollection()
            .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
            .AddLogging()
            .AddRouting()
            .AddOptions()
            .BuildServiceProvider();
        return services.GetRequiredService<TreeRouteBuilder>();
    }
 
    private static Mock<IRouter> CreateHandler()
    {
        var handler = new Mock<IRouter>(MockBehavior.Strict);
        handler
            .Setup(h => h.RouteAsync(It.IsAny<RouteContext>()))
            .Callback<RouteContext>(c => c.Handler = NullHandler)
            .Returns(Task.CompletedTask)
            .Verifiable();
        return handler;
    }
 
    private static Mock<IActionDescriptorCollectionProvider> CreateActionDescriptorProvider(
        IReadOnlyList<ActionDescriptor> actions)
    {
        var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(MockBehavior.Strict);
        actionDescriptorProvider
            .SetupGet(ad => ad.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(actions, version: 1));
 
        return actionDescriptorProvider;
    }
 
    private static AttributeRoute CreateRoute(
        IRouter handler,
        IActionDescriptorCollectionProvider actionDescriptorProvider)
    {
        return CreateRoute((_) => handler, actionDescriptorProvider);
    }
 
    private static AttributeRoute CreateRoute(
        Func<ActionDescriptor[], IRouter> handlerFactory,
        IActionDescriptorCollectionProvider actionDescriptorProvider)
    {
        var services = new ServiceCollection()
            .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
            .AddLogging()
            .AddRouting()
            .AddOptions()
            .BuildServiceProvider();
        return new AttributeRoute(actionDescriptorProvider, services, handlerFactory);
    }
 
    // Needed because new RouteValueDictionary(values) would give us all the properties of
    // the Dictionary class.
    private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, string> values)
    {
        var result = new RouteValueDictionary();
        foreach (var kvp in values)
        {
            result.Add(kvp.Key, kvp.Value);
        }
 
        return result;
    }
}