dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.02k forks source link

Routing with common prefix #58838

Open gfoidl opened 5 hours ago

gfoidl commented 5 hours ago

Discussed in https://github.com/dotnet/aspnetcore/discussions/58837

Originally posted by **MichalSznajder** November 8, 2024 Assume app with following routes: ``` app.MapGet("/first", () => "First"); app.MapGet("/{param}/second", () => "Second"); ``` In this scenario accessing `/first/second` will result in response `Second`. After some debugging I found following code in `DfaMarcher` https://github.com/dotnet/aspnetcore/blob/97c6f0df0d7b643dd5bac6f8a8641bd16738d2c5/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs#L330-L336 This results with `DfaMatcher` table looking like this: ``` /first/ HTTP: GET -> 'HTTP: GET /first' /first/ HTTP: * -> '405 HTTP Method Not Supported' /first/second/ HTTP: GET -> 'HTTP: GET /{param}/second' /first/second/ HTTP: * -> '405 HTTP Method Not Supported' /{...}/second/ HTTP: GET -> 'HTTP: GET /{param}/second' /{...}/second/ HTTP: * -> '405 HTTP Method Not Supported' ``` https://github.com/dotnet/aspnetcore/issues/38117 mentions > We've seen some issues with large DFAs as a result of some patterns that involve a large amount of variable prefixes. We've done work in 6.0 to address this via automatically trimming the DFA we generate by leveraging existing route constraints as well as the existing structure in complex segments. so I tried ``` app.MapGet("/first", () => "First"); app.MapGet("/{param:int}/second", () => "Second"); ``` that results in `DfaTable` of: ``` /first/ HTTP: GET -> 'HTTP: GET /first' /first/ HTTP: * -> '405 HTTP Method Not Supported' /{...}/second/ HTTP: GET -> 'HTTP: GET /{param}/second' /{...}/second/ HTTP: * -> '405 HTTP Method Not Supported' ``` However I would like `param` to be anything and `/first/second` return "First". Is there a way to change it in automatic way?
MichalSznajder commented 4 hours ago

I just found in https://github.com/dotnet/aspnetcore/issues/38468 that there exists an interface IParameterLiteralNodeMatchingPolicy.

I implemented constrain:

public class UniqueRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
{
    public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        return true;
    }

    public bool MatchesLiteral(string parameterName, string literal)
    {
        return false;
    }
}

and registered it

        services.AddRouting(options =>
        {
            options.ConstraintMap.Add("unique", typeof(UniqueRouteConstraint));
        });

This seems to solve my issue.

But question remains why router behaves like that? Is it intentional or it is a backward compatibility with ASP MVC? Also note that decorating each "{param}" with "{param:unique}" to avoid this behavior is quite tedious...

CC @javiercn since you introduced IParameterLiteralNodeMatchingPolicy in https://github.com/dotnet/aspnetcore/issues/35042

javiercn commented 1 hour ago

However I would like param to be anything and /first/second return "First".

This will never match the path /first

javiercn commented 37 minutes ago

But question remains why router behaves like that? Is it intentional or it is a backward compatibility with ASP MVC? Also note that decorating each "{param}" with "{param:unique}" to avoid this behavior is quite tedious...

This is intentional, the two routes on the original example will match different things. /first can only match the first route, /first/second can only match the second route

The optimization that we did implies that you will never want to match /first/second to the second route, but that would result in a 404, not in matching /first.

If you want to match /first/second then you are stuck with

app.MapGet("/first", () => "First");
app.MapGet("/{param}/second", () => "Second");

Our routing algorithm trades-off memory for CPU time so that matching is always linear based on the number of segments in the path.

I would add though that it's not easy to run into this case (for the vast majority of apps we've seen it's fine) so you shouldn't be trying to apply this technique unless you actually have a problem.

Also, if you wanted to apply this technique programmatically, there are ways to do so, like using an endpoint builder convention to programmatically modify the routes, for example based on the parameter name.