File: Routing\ControllerActionEndpointDataSourceTest.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.Builder;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.Routing;
 
public class ControllerActionEndpointDataSourceTest : ActionEndpointDataSourceBaseTest
{
    [Fact]
    public void Endpoints_Ignores_NonController()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
            {
                new ActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/test",
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Test" },
                        { "controller", "Test" },
                    },
                },
            };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
 
        // Act
        var endpoints = dataSource.Endpoints;
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void Endpoints_MultipledActions_MultipleRoutes()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
            {
                new ControllerActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/test",
                        Name = "Test",
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Test" },
                        { "controller", "Test" },
                    },
                },
                new ControllerActionDescriptor
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Index" },
                        { "controller", "Home" },
                    },
                }
            };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider
            .Setup(m => m.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
        dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
        dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
 
        // Act
        var endpoints = dataSource.Endpoints;
 
        // Assert
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
            });
 
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("1", e.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
                Assert.Equal("1", e.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("2", e.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
                Assert.Equal("2", e.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
            },
            e =>
            {
                Assert.Equal("/test", e.RoutePattern.RawText);
                Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Test", e.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
                Assert.Equal("Test", e.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
            });
    }
 
    [Fact]
    public void Endpoints_AppliesConventions()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
            {
                new ControllerActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/test",
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Test" },
                        { "controller", "Test" },
                    },
                },
                new ControllerActionDescriptor
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Index" },
                        { "controller", "Home" },
                    },
                }
            };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
        dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
        dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
 
        dataSource.DefaultBuilder.Add((b) =>
        {
            b.Metadata.Add("Hi there");
        });
 
        // Act
        var endpoints = dataSource.Endpoints;
 
        // Assert
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
            });
 
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/test", e.RoutePattern.RawText);
                Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
            });
    }
 
    [Fact]
    public void Endpoints_AppliesConventions_CanOverideEndpointName()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
            {
                new ControllerActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/test",
                        Name = "Test",
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Test" },
                        { "controller", "Test" },
                    },
                },
                new ControllerActionDescriptor
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Index" },
                        { "controller", "Home" },
                    },
                }
            };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider
            .Setup(m => m.ActionDescriptors)
            .Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
        dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
        dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
 
        dataSource.DefaultBuilder.Add(b =>
        {
            if (b.Metadata.OfType<ActionDescriptor>().FirstOrDefault()?.AttributeRouteInfo != null)
            {
                b.Metadata.Add(new EndpointNameMetadata("NewName"));
            }
        });
 
        // Act
        var endpoints = dataSource.Endpoints;
 
        // Assert
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
            });
 
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("1", e.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
                Assert.Equal("1", e.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("2", e.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
                Assert.Equal("2", e.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
            },
            e =>
            {
                Assert.Equal("/test", e.RoutePattern.RawText);
                Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Test", e.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
                Assert.Equal("NewName", e.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
            });
    }
 
    [Fact]
    public void Endpoints_AppliesConventions_RouteSpecificMetadata()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
            {
                new ControllerActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/test",
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Test" },
                        { "controller", "Test" },
                    },
                },
                new ControllerActionDescriptor
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Index" },
                        { "controller", "Home" },
                    },
                }
            };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
        dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null).Add(b => b.Metadata.Add("A"));
        dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null).Add(b => b.Metadata.Add("B"));
 
        dataSource.DefaultBuilder.Add((b) =>
        {
            b.Metadata.Add("Hi there");
        });
 
        // Act
        var endpoints = dataSource.Endpoints;
 
        // Assert
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "Hi there", "A" }, e.Metadata.GetOrderedMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "Hi there", "B" }, e.Metadata.GetOrderedMetadata<string>());
            });
 
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "Hi there", "A" }, e.Metadata.GetOrderedMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "Hi there", "B" }, e.Metadata.GetOrderedMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/test", e.RoutePattern.RawText);
                Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal("Hi there", e.Metadata.GetMetadata<string>());
            });
    }
 
    [Fact]
    public void GroupedEndpoints_AppliesConventions_RouteSpecificMetadata()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
        {
            new ControllerActionDescriptor
            {
                AttributeRouteInfo = new AttributeRouteInfo()
                {
                    Template = "/test",
                },
                RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    { "action", "Test" },
                    { "controller", "Test" },
                },
                EndpointMetadata = new[] { "A" },
            },
            new ControllerActionDescriptor
            {
                RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    { "action", "Index" },
                    { "controller", "Home" },
                },
            }
        };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
        dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null).Add(b => b.Metadata.Add("A"));
        dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null).Add(b => b.Metadata.Add("B"));
 
        dataSource.DefaultBuilder.Add((b) =>
        {
            b.Metadata.Add("Hi there");
        });
 
        // Act
        var groupConventions = new List<Action<EndpointBuilder>>()
        {
            b => b.Metadata.Add(new GroupMetadata()),
            b => b.Metadata.Add("group")
        };
        var sp = Mock.Of<IServiceProvider>();
        var groupPattern = RoutePatternFactory.Parse("/group1");
        var endpoints = dataSource.GetGroupedEndpoints(new RouteGroupContext
        {
            Prefix = groupPattern,
            Conventions = groupConventions,
            ApplicationServices = sp
        });
 
        // Assert
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => !SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/group1/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "group", "Hi there", "A" }, e.Metadata.GetOrderedMetadata<string>());
                Assert.NotNull(e.Metadata.GetMetadata<GroupMetadata>());
            },
            e =>
            {
                Assert.Equal("/group1/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Same(actions[1], e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "group", "Hi there", "B" }, e.Metadata.GetOrderedMetadata<string>());
                Assert.NotNull(e.Metadata.GetMetadata<GroupMetadata>());
            });
 
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/group1/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                // Group conventions are applied first, then endpoint specific metadata, then normal conventions, then per route conventions
                Assert.Equal(new[] { "group", "Hi there", "A" }, e.Metadata.GetOrderedMetadata<string>());
                Assert.NotNull(e.Metadata.GetMetadata<GroupMetadata>());
            },
            e =>
            {
                Assert.Equal("/group1/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Null(e.Metadata.GetMetadata<ActionDescriptor>());
                Assert.Equal(new[] { "group", "Hi there", "B" }, e.Metadata.GetOrderedMetadata<string>());
                Assert.NotNull(e.Metadata.GetMetadata<GroupMetadata>());
            },
            e =>
            {
                Assert.Equal("/group1/test", e.RoutePattern.RawText);
                Assert.Same(actions[0], e.Metadata.GetMetadata<ActionDescriptor>());
                // Group conventions are applied first, then endpoint specific metadata, then normal conventions
                Assert.Equal(new[] { "group", "A", "Hi there" }, e.Metadata.GetOrderedMetadata<string>());
                Assert.NotNull(e.Metadata.GetMetadata<GroupMetadata>());
            });
    }
 
    [Fact]
    public void Endpoints_AppliesFinallyConventions_InFIFOOrder_Last()
    {
        // Arrange
        var actions = new List<ActionDescriptor>
            {
                new ControllerActionDescriptor
                {
                    AttributeRouteInfo = new AttributeRouteInfo()
                    {
                        Template = "/test",
                    },
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Test" },
                        { "controller", "Test" },
                    },
                },
                new ControllerActionDescriptor
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        { "action", "Index" },
                        { "controller", "Home" },
                    },
                }
            };
 
        var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
        mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(actions, 0));
 
        var dataSource = (ControllerActionEndpointDataSource)CreateDataSource(mockDescriptorProvider.Object);
        var builder1 = dataSource.AddRoute("1", "/1/{controller}/{action}/{id?}", null, null, null);
        builder1.Finally(b => b.Metadata.Add("A1"));
        builder1.Finally(b => b.Metadata.Add("A2"));
        var builder2 = dataSource.AddRoute("2", "/2/{controller}/{action}/{id?}", null, null, null);
        builder2.Finally(b => b.Metadata.Add("B1"));
        builder2.Finally(b => b.Metadata.Add("B2"));
 
        dataSource.DefaultBuilder.Finally(b => b.Metadata.Add("C1"));
        dataSource.DefaultBuilder.Finally(b => b.Metadata.Add("C2"));
 
        // Act
        var endpoints = dataSource.Endpoints;
 
        // Assert
        Assert.Collection(
            endpoints.OfType<RouteEndpoint>().Where(e => SupportsLinkGeneration(e)).OrderBy(e => e.RoutePattern.RawText),
            e =>
            {
                Assert.Equal("/1/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Equal(new[] { "A1", "A2", "C1", "C2" }, e.Metadata.GetOrderedMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/2/{controller}/{action}/{id?}", e.RoutePattern.RawText);
                Assert.Equal(new[] { "B1", "B2", "C1", "C2" }, e.Metadata.GetOrderedMetadata<string>());
            },
            e =>
            {
                Assert.Equal("/test", e.RoutePattern.RawText);
                Assert.Equal(new[] { "C1", "C2" }, e.Metadata.GetOrderedMetadata<string>());
            });
    }
 
    private class GroupMetadata { }
 
    private static bool SupportsLinkGeneration(RouteEndpoint endpoint)
    {
        return !(endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true);
    }
 
    private protected override ActionEndpointDataSourceBase CreateDataSource(IActionDescriptorCollectionProvider actions, ActionEndpointFactory endpointFactory)
    {
        return new ControllerActionEndpointDataSource(
            new ControllerActionEndpointDataSourceIdProvider(),
            actions,
            endpointFactory,
            new OrderedEndpointsSequenceProvider());
    }
 
    protected override ActionDescriptor CreateActionDescriptor(
        object values,
        string pattern = null,
        IList<object> metadata = null)
    {
        var action = new ControllerActionDescriptor();
 
        foreach (var kvp in new RouteValueDictionary(values))
        {
            action.RouteValues[kvp.Key] = kvp.Value?.ToString();
        }
 
        if (!string.IsNullOrEmpty(pattern))
        {
            action.AttributeRouteInfo = new AttributeRouteInfo
            {
                Name = "test",
                Template = pattern,
            };
        }
 
        action.EndpointMetadata = metadata;
        return action;
    }
}