File: Builder\RouteHandlerEndpointRouteBuilderExtensionsTest.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.
 
#nullable enable
 
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.AspNetCore.Builder;
 
public class RouteHandlerEndpointRouteBuilderExtensionsTest : LoggedTest
{
    private RouteEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder)
    {
        return Assert.IsType<RouteEndpointDataSource>(Assert.Single(endpointRouteBuilder.DataSources));
    }
 
    private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder)
    {
        return GetBuilderEndpointDataSource(endpointRouteBuilder).GetSingleRouteEndpointBuilder();
    }
 
    public static object?[]?[] MapMethods
    {
        get
        {
            IEndpointConventionBuilder MapGet(IEndpointRouteBuilder routes, string template, Delegate action) =>
                routes.MapGet(template, action);
 
            IEndpointConventionBuilder MapPost(IEndpointRouteBuilder routes, string template, Delegate action) =>
                routes.MapPost(template, action);
 
            IEndpointConventionBuilder MapPut(IEndpointRouteBuilder routes, string template, Delegate action) =>
                routes.MapPut(template, action);
 
            IEndpointConventionBuilder MapDelete(IEndpointRouteBuilder routes, string template, Delegate action) =>
                routes.MapDelete(template, action);
 
            IEndpointConventionBuilder MapPatch(IEndpointRouteBuilder routes, string template, Delegate action) =>
                routes.MapPatch(template, action);
 
            IEndpointConventionBuilder Map(IEndpointRouteBuilder routes, string template, Delegate action) =>
                routes.Map(template, action);
 
            return new object?[]?[]
            {
                    new object?[] { (Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder>)MapGet, "GET" },
                    new object?[] { (Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder>)MapPost, "POST" },
                    new object?[] { (Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder>)MapPut, "PUT" },
                    new object?[] { (Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder>)MapDelete, "DELETE" },
                    new object?[] { (Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder>)MapPatch, "PATCH" },
                    new object?[] { (Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder>)Map, null },
            };
        }
    }
 
    [Fact]
    public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
 
        [HttpMethod("ATTRIBUTE")]
        void TestAction()
        {
        }
 
        var endpointBuilder = builder.MapMethods("/", new[] { "METHOD" }, (Action)TestAction);
        endpointBuilder.WithMetadata(new HttpMethodMetadata(new[] { "BUILDER" }));
 
        var dataSource = Assert.Single(builder.DataSources);
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var metadataArray = endpoint.Metadata.OfType<IHttpMethodMetadata>().ToArray();
 
        static string GetMethod(IHttpMethodMetadata metadata) => Assert.Single(metadata.HttpMethods);
 
        Assert.Equal(3, metadataArray.Length);
        Assert.Equal("METHOD", GetMethod(metadataArray[0]));
        Assert.Equal("ATTRIBUTE", GetMethod(metadataArray[1]));
        Assert.Equal("BUILDER", GetMethod(metadataArray[2]));
 
        Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata<IHttpMethodMetadata>()!.HttpMethods.Single());
    }
 
    [Fact]
    public void MapGet_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("GET", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapPatch_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapPatch("/", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("PATCH", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public async Task MapGet_WithRouteParameter_BuildsEndpointWithRouteSpecificBinding()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/{id}", (int? id, HttpContext httpContext) =>
        {
            if (id is not null)
            {
                httpContext.Items["input"] = id;
            }
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("GET", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: GET /{id}", routeEndpointBuilder.DisplayName);
        Assert.Equal("/{id}", routeEndpointBuilder.RoutePattern.RawText);
 
        // Assert that we don't fallback to the query string
        var httpContext = new DefaultHttpContext();
 
        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
        {
            ["id"] = "42"
        });
 
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Null(httpContext.Items["input"]);
    }
 
    [Fact]
    public async Task MapGet_WithoutRouteParameter_BuildsEndpointWithQuerySpecificBinding()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/", (int? id, HttpContext httpContext) =>
        {
            if (id is not null)
            {
                httpContext.Items["input"] = id;
            }
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("GET", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
 
        // Assert that we don't fallback to the route values
        var httpContext = new DefaultHttpContext();
 
        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>()
        {
            ["id"] = "41"
        });
        httpContext.Request.RouteValues = new();
        httpContext.Request.RouteValues["id"] = "42";
 
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Equal(41, httpContext.Items["input"]);
    }
 
    [Fact]
    public void MapGet_ThrowsWithImplicitFromBody()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/", (Todo todo) => { });
        var dataSource = GetBuilderEndpointDataSource(builder);
        var ex = Assert.Throws<InvalidOperationException>(() => dataSource.Endpoints);
        Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message);
        Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromServices] or [FromBody] attribute?", ex.Message);
    }
 
    [Fact]
    public void MapDelete_ThrowsWithImplicitFromBody()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapDelete("/", (Todo todo) => { });
        var dataSource = GetBuilderEndpointDataSource(builder);
        var ex = Assert.Throws<InvalidOperationException>(() => dataSource.Endpoints);
        Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message);
        Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromServices] or [FromBody] attribute?", ex.Message);
    }
 
    public static object[][] NonImplicitFromBodyMethods
    {
        get
        {
            return new[]
            {
                    new[] { HttpMethods.Delete },
                    new[] { HttpMethods.Connect },
                    new[] { HttpMethods.Trace },
                    new[] { HttpMethods.Get },
                    new[] { HttpMethods.Head },
                    new[] { HttpMethods.Options },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(NonImplicitFromBodyMethods))]
    public void MapVerb_ThrowsWithImplicitFromBody(string method)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapMethods("/", new[] { method }, (Todo todo) => { });
        var dataSource = GetBuilderEndpointDataSource(builder);
        var ex = Assert.Throws<InvalidOperationException>(() => dataSource.Endpoints);
        Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message);
        Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromServices] or [FromBody] attribute?", ex.Message);
    }
 
    [Fact]
    public void MapGet_ImplicitFromService()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton<TodoService>().BuildServiceProvider()));
        _ = builder.MapGet("/", (TodoService todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("GET", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapDelete_ImplicitFromService()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton<TodoService>().BuildServiceProvider()));
        _ = builder.MapDelete("/", (TodoService todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("DELETE", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapPatch_ImplicitFromService()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton<TodoService>().BuildServiceProvider()));
        _ = builder.MapPatch("/", (TodoService todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("PATCH", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [AttributeUsage(AttributeTargets.Parameter)]
    private class TestFromServiceAttribute : Attribute, IFromServiceMetadata
    { }
 
    [Fact]
    public void MapGet_ExplicitFromService()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton<TodoService>().BuildServiceProvider()));
        _ = builder.MapGet("/", ([TestFromServiceAttribute] TodoService todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("GET", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapDelete_ExplicitFromService()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton<TodoService>().BuildServiceProvider()));
        _ = builder.MapDelete("/", ([TestFromServiceAttribute] TodoService todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("DELETE", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapPatch_ExplicitFromService()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton<TodoService>().BuildServiceProvider()));
        _ = builder.MapPatch("/", ([TestFromServiceAttribute] TodoService todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("PATCH", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [AttributeUsage(AttributeTargets.Parameter)]
    private class TestFromBodyAttribute : Attribute, IFromBodyMetadata
    { }
 
    [Fact]
    public void MapGet_ExplicitFromBody_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/", ([TestFromBody] Todo todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("GET", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapDelete_ExplicitFromBody_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapDelete("/", ([TestFromBody] Todo todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("DELETE", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapPatch_ExplicitFromBody_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapPatch("/", ([TestFromBody] Todo todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("PATCH", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Theory]
    [MemberData(nameof(MapMethods))]
    public void MapVerbDoesNotDuplicateMetadata(Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder> map, string expectedMethod)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
 
        map(builder, "/{ID}", () => { }).WithName("Foo");
 
        var dataSource = GetBuilderEndpointDataSource(builder);
 
        // Access endpoints a couple of times to make sure it gets built
        _ = dataSource.Endpoints;
        _ = dataSource.Endpoints;
        _ = dataSource.Endpoints;
 
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var endpointNameMetadata = Assert.Single(endpoint.Metadata.GetOrderedMetadata<IEndpointNameMetadata>());
        var routeNameMetadata = Assert.Single(endpoint.Metadata.GetOrderedMetadata<IRouteNameMetadata>());
        Assert.Equal("Foo", endpointNameMetadata.EndpointName);
        Assert.Equal("Foo", routeNameMetadata.RouteName);
 
        if (expectedMethod is not null)
        {
            var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
            Assert.NotNull(methodMetadata);
            var method = Assert.Single(methodMetadata!.HttpMethods);
            Assert.Equal(expectedMethod, method);
        }
    }
 
    [Theory]
    [MemberData(nameof(MapMethods))]
    public void AddingMetadataAfterBuildingEndpointThrows(Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder> map, string expectedMethod)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
 
        var endpointBuilder = map(builder, "/{ID}", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
 
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        if (expectedMethod is not null)
        {
            var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
            Assert.NotNull(methodMetadata);
            var method = Assert.Single(methodMetadata!.HttpMethods);
            Assert.Equal(expectedMethod, method);
        }
 
        Assert.Throws<InvalidOperationException>(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo")));
        Assert.Throws<InvalidOperationException>(() => endpointBuilder.Finally(b => b.Metadata.Add(new RouteNameMetadata("Foo"))));
    }
 
    [Theory]
    [MemberData(nameof(MapMethods))]
    public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder> map, string expectedMethod)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
 
        map(builder, "/{ID}", ([FromRoute] int? id, HttpContext httpContext) =>
        {
            if (id is not null)
            {
                httpContext.Items["input"] = id;
            }
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        if (expectedMethod is not null)
        {
            var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
            Assert.NotNull(methodMetadata);
            var method = Assert.Single(methodMetadata!.HttpMethods);
            Assert.Equal(expectedMethod, method);
        }
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        if (expectedMethod is not null)
        {
            Assert.Equal($"HTTP: {expectedMethod} /{{ID}}", routeEndpointBuilder.DisplayName);
        }
        Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText);
 
        var httpContext = new DefaultHttpContext();
 
        httpContext.Request.RouteValues["id"] = "13";
 
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Equal(13, httpContext.Items["input"]);
    }
 
    [Theory]
    [MemberData(nameof(MapMethods))]
    public async Task MapVerbWithRouteParameterDoesNotFallbackToQuery(Func<IEndpointRouteBuilder, string, Delegate, IEndpointConventionBuilder> map, string expectedMethod)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
 
        map(builder, "/{ID}", (int? id, HttpContext httpContext) =>
        {
            if (id is not null)
            {
                httpContext.Items["input"] = id;
            }
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
        if (expectedMethod is not null)
        {
            var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
            Assert.NotNull(methodMetadata);
            var method = Assert.Single(methodMetadata!.HttpMethods);
            Assert.Equal(expectedMethod, method);
        }
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        if (expectedMethod is not null)
        {
            Assert.Equal($"HTTP: {expectedMethod} /{{ID}}", routeEndpointBuilder.DisplayName);
        }
        Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText);
 
        // Assert that we don't fallback to the query string
        var httpContext = new DefaultHttpContext();
 
        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
        {
            ["id"] = "42"
        });
 
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Null(httpContext.Items["input"]);
    }
 
    [Fact]
    public void MapGetWithRouteParameter_ThrowsIfRouteParameterDoesNotExist()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/", ([FromRoute] int id) => { });
        var dataSource = GetBuilderEndpointDataSource(builder);
        var ex = Assert.Throws<InvalidOperationException>(() => dataSource.Endpoints);
        Assert.Equal("'id' is not a route parameter.", ex.Message);
    }
 
    [Fact]
    public async Task MapGetWithNamedFromRouteParameter_UsesFromRouteName()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/{value}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) =>
        {
            httpContext.Items["value"] = id;
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        // Assert that we don't fallback to the query string
        var httpContext = new DefaultHttpContext();
 
        httpContext.Request.RouteValues["value"] = "42";
 
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Equal(42, httpContext.Items["value"]);
    }
 
    [Fact]
    public async Task MapGetWithNamedFromRouteParameter_FailsForParameterName()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/{value}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) =>
        {
            httpContext.Items["value"] = id;
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        // Assert that we don't fallback to the query string
        var httpContext = new DefaultHttpContext();
        httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider();
 
        httpContext.Request.RouteValues["id"] = "42";
 
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Null(httpContext.Items["value"]);
        Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
    }
 
    [Fact]
    public void MapGetWithNamedFromRouteParameter_ThrowsForMismatchedPattern()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapGet("/{id}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => { });
        var dataSource = GetBuilderEndpointDataSource(builder);
        var ex = Assert.Throws<InvalidOperationException>(() => dataSource.Endpoints);
        Assert.Equal("'value' is not a route parameter.", ex.Message);
    }
 
    [Fact]
    public void MapPost_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapPost("/", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("POST", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: POST /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapPost_BuildsEndpointWithCorrectEndpointMetadata()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapPost("/", [TestConsumesAttribute(typeof(Todo), "application/xml")] (Todo todo) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var endpointMetadata = endpoint.Metadata.GetMetadata<IAcceptsMetadata>();
 
        Assert.NotNull(endpointMetadata);
        Assert.False(endpointMetadata!.IsOptional);
        Assert.Equal(typeof(Todo), endpointMetadata.RequestType);
        Assert.Equal(new[] { "application/xml" }, endpointMetadata.ContentTypes);
    }
 
    [Fact]
    public void MapPut_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapPut("/", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("PUT", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: PUT /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapDelete_BuildsEndpointWithCorrectMethod()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapDelete("/", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        Assert.NotNull(methodMetadata);
        var method = Assert.Single(methodMetadata!.HttpMethods);
        Assert.Equal("DELETE", method);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
    }
 
    [Fact]
    public void MapFallback_BuildsEndpointWithLowestRouteOrder()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapFallback("/", () => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("Fallback /", routeEndpointBuilder.DisplayName);
        Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
        Assert.Equal(int.MaxValue, routeEndpointBuilder.Order);
    }
 
    [Fact]
    public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapFallback(() => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Equal("Fallback {*path:nonfile}", routeEndpointBuilder.DisplayName);
        Assert.Equal("{*path:nonfile}", routeEndpointBuilder.RoutePattern.RawText);
        Assert.Single(routeEndpointBuilder.RoutePattern.Parameters);
        Assert.True(routeEndpointBuilder.RoutePattern.Parameters[0].IsCatchAll);
        Assert.Equal(int.MaxValue, routeEndpointBuilder.Order);
        Assert.Contains(FallbackMetadata.Instance, routeEndpointBuilder.Metadata);
    }
 
    [Fact]
    public void WithTags_CanSetTagsForEndpoint()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        string GetString() => "Foo";
        _ = builder.MapDelete("/", GetString).WithTags("Some", "Test", "Tags");
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var tagsMetadata = endpoint.Metadata.GetMetadata<ITagsMetadata>();
        Assert.Equal(new[] { "Some", "Test", "Tags" }, tagsMetadata?.Tags);
    }
 
    [Fact]
    public void MapMethod_DoesNotEndpointNameForMethodGroupByDefault()
    {
        string GetString() => "Foo";
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider()));
        _ = builder.MapDelete("/", GetString);
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
        var routeName = endpoint.Metadata.GetMetadata<IRouteNameMetadata>();
        var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
        Assert.Null(endpointName);
        Assert.Null(routeName);
        Assert.Equal("HTTP: DELETE / => GetString", routeEndpointBuilder.DisplayName);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task MapMethod_FlowsThrowOnBadHttpRequest(bool throwOnBadRequest)
    {
        var serviceProvider = new EmptyServiceProvider();
        serviceProvider.RouteHandlerOptions.ThrowOnBadRequest = throwOnBadRequest;
 
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
        _ = builder.Map("/{id}", (int id) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var httpContext = new DefaultHttpContext();
        httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider();
        httpContext.Request.RouteValues["id"] = "invalid!";
 
        if (throwOnBadRequest)
        {
            var ex = await Assert.ThrowsAsync<BadHttpRequestException>(() => endpoint.RequestDelegate!(httpContext));
            Assert.Equal(400, ex.StatusCode);
        }
        else
        {
            await endpoint.RequestDelegate!(httpContext);
            Assert.Equal(400, httpContext.Response.StatusCode);
        }
    }
 
    [Fact]
    public async Task MapMethod_DefaultsToNotThrowOnBadHttpRequestIfItCannotResolveRouteHandlerOptions()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()));
 
        _ = builder.Map("/{id}", (int id) => { });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var httpContext = new DefaultHttpContext();
        httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider();
        httpContext.Request.RouteValues["id"] = "invalid!";
 
        await endpoint.RequestDelegate!(httpContext);
        Assert.Equal(400, httpContext.Response.StatusCode);
    }
 
    public static object[][] AddFiltersByClassData =
    {
        new object[] { (Action<IEndpointConventionBuilder>)((IEndpointConventionBuilder builder) => builder.AddEndpointFilter(new IncrementArgFilter())) },
        new object[] { (Action<IEndpointConventionBuilder>)((IEndpointConventionBuilder builder) => builder.AddEndpointFilter<IEndpointConventionBuilder, IncrementArgFilter>()) }
    };
 
    public static object[][] AddFiltersByDelegateData
    {
        get
        {
            void WithFilter(IEndpointConventionBuilder builder) =>
                builder.AddEndpointFilter(async (context, next) =>
                {
                    context.Arguments[0] = ((int)context.Arguments[0]!) + 1;
                    return await next(context);
                });
 
            void WithFilterFactory(IEndpointConventionBuilder builder) =>
                builder.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) =>
                {
                    Assert.NotNull(routeHandlerContext.MethodInfo);
                    Assert.NotNull(routeHandlerContext.MethodInfo.DeclaringType);
                    Assert.NotNull(routeHandlerContext.ApplicationServices);
                    Assert.Equal("RouteHandlerEndpointRouteBuilderExtensionsTest", routeHandlerContext.MethodInfo.DeclaringType?.Name);
                    context.Arguments[0] = context.GetArgument<int>(0) + 1;
                    return await next(context);
                });
 
            return new object[][] {
                new object[] { (Action<IEndpointConventionBuilder>)WithFilter },
                new object[] { (Action<IEndpointConventionBuilder>)WithFilterFactory  }
            };
        }
    }
 
    private static async Task AssertIdAsync(Endpoint endpoint, string expectedPattern, int expectedId)
    {
        var routeEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
        Assert.Equal(expectedPattern, routeEndpoint.RoutePattern.RawText);
 
        var httpContext = new DefaultHttpContext();
        httpContext.RequestServices = new ServiceCollection().BuildServiceProvider();
        httpContext.Request.RouteValues["id"] = "2";
        var outStream = new MemoryStream();
        httpContext.Response.Body = outStream;
 
        await routeEndpoint.RequestDelegate!(httpContext);
 
        // Assert;
        var httpResponse = httpContext.Response;
        httpResponse.Body.Seek(0, SeekOrigin.Begin);
        var streamReader = new StreamReader(httpResponse.Body);
        var body = streamReader.ReadToEndAsync().Result;
        Assert.Equal(200, httpContext.Response.StatusCode);
        Assert.Equal($"ID: {expectedId}", body);
    }
 
    [Theory]
    [MemberData(nameof(AddFiltersByClassData))]
    [MemberData(nameof(AddFiltersByDelegateData))]
    public async Task AddEndpointFilterMethods_CanRegisterFilterWithClassAndDelegateImplementations(Action<IEndpointConventionBuilder> addFilter)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()));
 
        string PrintId(int id) => $"ID: {id}";
        addFilter(builder.Map("/{id}", PrintId));
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
        await AssertIdAsync(endpoint, "/{id}", 3);
    }
 
    [Theory]
    [MemberData(nameof(AddFiltersByClassData))]
    [MemberData(nameof(AddFiltersByDelegateData))]
    public async Task AddEndpointFilterMethods_WorkWithMapGroup(Action<IEndpointConventionBuilder> addFilter)
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()));
 
        string PrintId(int id) => $"ID: {id}";
        addFilter(builder.Map("/{id}", PrintId));
 
        var outerGroup = builder.MapGroup("/outer");
        addFilter(outerGroup);
        addFilter(outerGroup.Map("/{id}", PrintId));
 
        var innerGroup = outerGroup.MapGroup("/inner");
        addFilter(innerGroup);
        addFilter(innerGroup.Map("/{id}", PrintId));
 
        var endpoints = builder.DataSources
            .SelectMany(ds => ds.Endpoints)
            .ToDictionary(e => ((RouteEndpoint)e).RoutePattern.RawText!);
 
        Assert.Equal(3, endpoints.Count);
 
        // For each layer of grouping, another filter is applies which increments the expectedId by 1 each time.
        await AssertIdAsync(endpoints["/{id}"], expectedPattern: "/{id}", expectedId: 3);
        await AssertIdAsync(endpoints["/outer/{id}"], expectedPattern: "/outer/{id}", expectedId: 4);
        await AssertIdAsync(endpoints["/outer/inner/{id}"], expectedPattern: "/outer/inner/{id}", expectedId: 5);
    }
 
    [Fact]
    public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatAccessesServices()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()));
 
        string? PrintLogger(HttpContext context) => $"loggerErrorIsEnabled: {context.Items["loggerErrorIsEnabled"]}, parentName: {context.Items["parentName"]}";
        var routeHandlerBuilder = builder.Map("/", PrintLogger);
        routeHandlerBuilder.AddEndpointFilter<ServiceAccessingEndpointFilter>();
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        var endpoint = Assert.Single(dataSource.Endpoints);
 
        var httpContext = new DefaultHttpContext();
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton(LoggerFactory);
        httpContext.RequestServices = serviceCollection.BuildServiceProvider();
        var outStream = new MemoryStream();
        httpContext.Response.Body = outStream;
        await endpoint.RequestDelegate!(httpContext);
 
        Assert.Equal(200, httpContext.Response.StatusCode);
        var httpResponse = httpContext.Response;
        httpResponse.Body.Seek(0, SeekOrigin.Begin);
        var streamReader = new StreamReader(httpResponse.Body);
        var body = streamReader.ReadToEndAsync().Result;
        Assert.Equal("loggerErrorIsEnabled: True, parentName: RouteHandlerEndpointRouteBuilderExtensionsTest", body);
    }
 
    [Fact]
    public void RequestDelegateFactory_ProvidesAppServiceProvider_ToFilterFactory()
    {
        var appServiceCollection = new ServiceCollection();
        var appService = new MyService();
        appServiceCollection.AddSingleton(appService);
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(appServiceCollection.BuildServiceProvider()));
        var filterFactoryRan = false;
 
        string? PrintLogger(HttpContext context) => $"loggerErrorIsEnabled: {context.Items["loggerErrorIsEnabled"]}, parentName: {context.Items["parentName"]}";
        var routeHandlerBuilder = builder.Map("/", PrintLogger);
        routeHandlerBuilder.AddEndpointFilterFactory((rhc, next) =>
        {
            Assert.NotNull(rhc.ApplicationServices);
            var myService = rhc.ApplicationServices.GetRequiredService<MyService>();
            Assert.Equal(appService, myService);
            filterFactoryRan = true;
            return next;
        });
 
        var dataSource = GetBuilderEndpointDataSource(builder);
        // Trigger Endpoint build by calling getter.
        Assert.Single(dataSource.Endpoints);
        Assert.True(filterFactoryRan);
    }
 
    [Fact]
    public void FinallyOnGroup_CanExamineFinallyOnEndpoint()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()));
 
        var group = builder.MapGroup("/group");
        ((IEndpointConventionBuilder)group).Finally(b =>
        {
            if (b.Metadata.Any(md => md is string smd && smd == "added-from-endpoint"))
            {
                b.Metadata.Add("added-from-group");
            }
        });
 
        group.MapGet("/endpoint", () => { }).Finally(b => b.Metadata.Add("added-from-endpoint"));
 
        var endpoint = Assert.Single(builder.DataSources
            .SelectMany(ds => ds.Endpoints));
 
        Assert.Equal(new[] { "added-from-endpoint", "added-from-group" }, endpoint.Metadata.GetOrderedMetadata<string>());
    }
 
    [Fact]
    public void FinallyOnNestedGroups_OuterGroupCanExamineInnerGroup()
    {
        var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()));
 
        var outerGroup = builder.MapGroup("/group");
        var innerGroup = outerGroup.MapGroup("/");
        ((IEndpointConventionBuilder)innerGroup).Finally(b =>
        {
            // Verifies that both endpoint-specific finally conventions have run
            if (b.Metadata.Any(md => md is string smd && smd == "added-from-endpoint-1")
                && b.Metadata.Any(md => md is string smd && smd == "added-from-endpoint-2"))
            {
                b.Metadata.Add("added-from-inner-group");
            }
        });
        ((IEndpointConventionBuilder)outerGroup).Finally(b =>
        {
            if (b.Metadata.Any(md => md is string smd && smd == "added-from-inner-group"))
            {
                b.Metadata.Add("added-from-outer-group");
            }
        });
 
        var handler = innerGroup.MapGet("/endpoint", () => { });
        handler.Finally(b => b.Metadata.Add("added-from-endpoint-1"));
        handler.Finally(b => b.Metadata.Add("added-from-endpoint-2"));
 
        var endpoint = Assert.Single(builder.DataSources
            .SelectMany(ds => ds.Endpoints));
 
        Assert.Equal(new[] { "added-from-endpoint-1", "added-from-endpoint-2", "added-from-inner-group", "added-from-outer-group" }, endpoint.Metadata.GetOrderedMetadata<string>());
    }
 
    class MyService { }
 
    class ServiceAccessingEndpointFilter : IEndpointFilter
    {
        private ILogger _logger;
        private EndpointFilterFactoryContext _routeHandlerContext;
 
        public ServiceAccessingEndpointFilter(ILoggerFactory loggerFactory, EndpointFilterFactoryContext routeHandlerContext)
        {
            _logger = loggerFactory.CreateLogger<ServiceAccessingEndpointFilter>();
            _routeHandlerContext = routeHandlerContext;
        }
 
        public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
        {
            context.HttpContext.Items["loggerErrorIsEnabled"] = _logger.IsEnabled(LogLevel.Error);
            context.HttpContext.Items["parentName"] = _routeHandlerContext.MethodInfo.DeclaringType?.Name;
            return await next(context);
        }
    }
 
    class IncrementArgFilter : IEndpointFilter
    {
        public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
        {
            context.Arguments[0] = ((int)context.Arguments[0]!) + 1;
            return await next(context);
        }
    }
 
    class FromRoute : Attribute, IFromRouteMetadata
    {
        public string? Name { get; set; }
    }
 
    class TestConsumesAttribute : Attribute, IAcceptsMetadata
    {
        public TestConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes)
        {
            ArgumentNullException.ThrowIfNull(contentType);
 
            var contentTypes = new List<string>()
                {
                    contentType
                };
 
            for (var i = 0; i < otherContentTypes.Length; i++)
            {
                contentTypes.Add(otherContentTypes[i]);
            }
 
            _requestType = requestType;
            _contentTypes = contentTypes;
        }
 
        IReadOnlyList<string> IAcceptsMetadata.ContentTypes => _contentTypes;
        Type? IAcceptsMetadata.RequestType => _requestType;
 
        bool IAcceptsMetadata.IsOptional => false;
 
        Type? _requestType;
 
        List<string> _contentTypes = new();
    }
 
    class Todo
    {
 
    }
 
    // Here to more easily disambiguate when ToDo is
    // intended to be validated as an implicit service in tests
    class TodoService
    {
 
    }
 
    private class HttpMethodAttribute : Attribute, IHttpMethodMetadata
    {
        public bool AcceptCorsPreflight => false;
 
        public IReadOnlyList<string> HttpMethods { get; }
 
        public HttpMethodAttribute(params string[] httpMethods)
        {
            HttpMethods = httpMethods;
        }
    }
 
    private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory
    {
        public IServiceProvider ServiceProvider => this;
 
        public RouteHandlerOptions RouteHandlerOptions { get; set; } = new RouteHandlerOptions();
 
        public IServiceScope CreateScope()
        {
            return this;
        }
 
        public void Dispose()
        {
        }
 
        public object? GetService(Type serviceType)
        {
            if (serviceType == typeof(IServiceScopeFactory))
            {
                return this;
            }
            else if (serviceType == typeof(IOptions<RouteHandlerOptions>))
            {
                return Options.Create(RouteHandlerOptions);
            }
 
            return null;
        }
    }
}