Search code examples
c#asp.net-coreminimal-apisasp.net-minimal-apis

In ASP.NET Core minimal API, how can you get the route name?


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";
}

Solution

  • 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.