0

I would like to test the routing of a minimal API WebApplication. How can I get the route name, given a HttpRequestMessage?

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRouting().Configure<RouteOptions>(options => {});
var app = builder.Build();

app.MapGet("/hello", () => "Hello!").WithName(RouteNames.Hello);
app.MapGet("/world", () => "World!").WithName(RouteNames.World);

//app.Run();

var request = new HttpRequestMessage(HttpMethod.Get, "/world");
var routeName = GetRouteName(app, request);
Console.WriteLine($"routeName: {routeName}");

string? GetRouteName(WebApplication app, HttpRequestMessage request)
{
    return null; // TODO: Implement
}

static class RouteNames
{
    public const string Hello = "Hello";
    public const string World = "World";
}

Put in a feature request at https://github.com/dotnet/aspnetcore/issues/48034 .


I accepted Chris's answer. It got me on the right track. I'd like to be able to get the endpoint name without executing the actual endpoint, but I'll save that for another day. May be I just don't call next. Using his answer, we can stuff the endpoint into the response. A test harness can be created to test all of our named endpoints. Here is working code that uses <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.5" />:


using Microsoft.AspNetCore.TestHost;
using System.Diagnostics;

var host = new HostBuilder()
    .ConfigureWebHost(webHost => webHost.UseTestServer().Configure(app =>
    {
        app.UseRouting();
        app.Use((context, next) =>
        {
            if (context.GetEndpoint() is Endpoint endpoint)
            {
                var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName;
                if (endpointName != null)
                {
                    context.Response.Headers.Add(TagKeys.EndpointName, endpointName);
                }
            }
            return next();
        });
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/hello", () => "Hello!").WithName(RouteNames.Hello);
            endpoints.MapGet("/world", () => "World!").WithName(RouteNames.World);
        });
    })
    .ConfigureServices(services =>
    {
        services.AddRouting().Configure<RouteOptions>(routeOptions => { });
    }
    )
    ).Build();
host.Start();
var httpClient = host.GetTestClient();

await PrintEndpointName(httpClient, new HttpRequestMessage(HttpMethod.Get, "/"));
await PrintEndpointName(httpClient, new HttpRequestMessage(HttpMethod.Get, "/hello"));
await PrintEndpointName(httpClient, new HttpRequestMessage(HttpMethod.Get, "/world"));

async Task PrintEndpointName(HttpClient httpClient, HttpRequestMessage request)
{
    var httpResponse = await httpClient.SendAsync(request);
    IEnumerable<string>? headers;
    httpResponse.Headers.TryGetValues(TagKeys.EndpointName, out headers);
    var endpointName = headers?.FirstOrDefault();
    Debug.WriteLine($"{((int)httpResponse.StatusCode)} {endpointName}");
}

static class RouteNames
{
    public const string Hello = "Hello";
    public const string World = "World";
}

static class TagKeys
{
    public const string EndpointName = "endpoint.name";
}
Cameron Taggart
  • 5,771
  • 4
  • 45
  • 70
  • 1
    Personally I would just write [integration test](https://stackoverflow.com/a/70095604/2501279). – Guru Stron May 02 '23 at 09:14
  • This is an integration test to begin with. Mapping requests to routes is the job of the routing middleware which means you do need to configure the routes and start the routing middleware by starting the app. I'm sure the ASP.NET Core Github repo unit tests for the routing middleware, but that requires writing a lot of very different code than just `app.MapGet("/hello", () => "Hello!").WithName(RouteNames.Hello);` – Panagiotis Kanavos May 02 '23 at 09:22
  • On the other hand, you may be able to ask `app` for all `EndpointDataSource` objects. [This article](https://www.meziantou.net/list-all-routes-in-an-asp-net-core-application.htm) shows how to add an endpoint that returns all configured routes for development purposes. You may be able to use the same technique in your unit tests eg `var routes=app.Services.GetService>();` – Panagiotis Kanavos May 02 '23 at 09:32
  • You still need to call `app.Run` or at least `app.StartAsync` to generate the endpoints though. – Panagiotis Kanavos May 02 '23 at 09:49

1 Answers1

2

It's not 100% clear if you intend to use this on the server side or the client side. It would appear to be the server side. If you're on the server side, then you can get the mapping - when defined - without any additional lookup.

When matched, here is the world's simplest middleware that would capture the name of the endpoint/route that was invoked, if any:

app.Use( ( context, next ) =>
{
    if ( context.GetEndpoint() is Endpoint endpoint )
    {
        var name = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName ?? "(no name)";

        Debug.WriteLine( $"Endpoint = {name}" );
    }

    return next();
} );

In the context of Minimal APIs, WithName will produce both IEndpointNameMetadata.EndpointName and IRouteNameMetadata.RouteName. You can use either or add more defense to cover both. This approach should work for controller-based APIs as well.

It terms of a reverse lookup for a route/endpoint name from an URL, that is not supported and, honestly, cannot really work as expected. For example, if the incoming request URL is values/42, this will never match the defined route template values/{id}. A constant route template would work, but is rare.

I don't want to discourage you from saying it's impossible, but this is really, really hard, likely brittle, and probably not worth the effort. The best option IMHO is to let the routing system do its thing and then pull the metadata you're interested from the matched endpoint, if any.

Chris Martinez
  • 3,185
  • 12
  • 28