Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get the route name given an http method & uri #48034

Closed
1 task done
cataggar opened this issue May 2, 2023 · 4 comments
Closed
1 task done

get the route name given an http method & uri #48034

cataggar opened this issue May 2, 2023 · 4 comments

Comments

@cataggar
Copy link

cataggar commented May 2, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I'm using WithName to set the operation ID from the API specification. Given an HTTP request, I want to return that name. I only want to use the routing code and not the whole WebApplication plumbing.

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

Previously posted at https://stackoverflow.com/questions/76153365/in-asp-net-core-minimal-api-how-can-you-get-the-route-name , but now feels like this is a feature request.

WithName expects an operation ID.

    /// <summary>
    /// Adds the <see cref="IEndpointNameMetadata"/> to the Metadata collection for all endpoints produced
    /// on the target <see cref="IEndpointConventionBuilder"/> given the <paramref name="endpointName" />.
    /// The <see cref="IEndpointNameMetadata" /> on the endpoint is used for link generation and
    /// is treated as the operation ID in the given endpoint's OpenAPI specification.
    /// </summary>
    /// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
    /// <param name="endpointName">The endpoint name.</param>
    /// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
    public static TBuilder WithName<TBuilder>(this TBuilder builder, string endpointName) where TBuilder : IEndpointConventionBuilder
    {
        builder.WithMetadata(new EndpointNameMetadata(endpointName), new RouteNameMetadata(endpointName));
        return builder;
    }

Describe the solution you'd like

Something that wires up the routing other than app.Run() and provides access to the routing.

Additional context

No response

@davidfowl
Copy link
Member

I only want to use the routing code and not the whole WebApplication plumbing.

Can you clarify? Where are you trying to get this information? What does it have to do with the HttpRequestMessage?

@cataggar
Copy link
Author

cataggar commented May 3, 2023

I need this to clean up telemetry in a couple of places for our Azure Resource Provider. When an Activity is created, you pass in an OperationName. When these activities are created, I have the request HTTP method and URI. That should be enough to get back the route name. I'm hoping just the ASP.NET routing can be used to get this.

HttpRequestMessage is not needed. string can be used for HTTP method and request URI:

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.StartAsync();

var endpointDataSource = app.Services.GetService<IEnumerable<EndpointDataSource>>()?.Single() as CompositeEndpointDataSource;

var routeName = GetRouteName(app, "GET", "http://localhost/world");
Console.WriteLine($"routeName: {routeName}");

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

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

I'll start a Teams chat to give more MS internal specifics.

@commonsensesoftware
Copy link

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 and API version 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)";
        var version = context.GetRequestedApiVersion();

        Debug.WriteLine( $"Endpoint = {name}, API Version = {version}" );
    }

    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.

Collating APIs for versioning faces a similar challenge, which is why APIs are not collated by route template; they are collated by logical name for controllers and an optionally named set for Minimal APIs. This is just the surface of the problem. When you throw versioning into the mix, you could end up with templates such as values/{id} and values/{id:int}, which are not the same, but are semantically equivalent.

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.

@cataggar
Copy link
Author

cataggar commented May 5, 2023

Thank you @commonsensesoftware to answer here and at https://stackoverflow.com/questions/76153365/in-asp-net-core-minimal-api-how-can-you-get-the-route-name . The solution you provided works for me. Combined with <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.5" /> I can get what I want:

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

I'd like to be able to skip the execution of the rest of the endpoint function. May be removing next() will get me there. Either way, I've got something that works now. Thanks!

@cataggar cataggar closed this as completed May 5, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Jun 4, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants