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.21k stars 9.95k forks source link

Endpoint routing extensibility #14619

Closed J-ohn closed 4 years ago

J-ohn commented 4 years ago

I am trying to extend endpoint routing in order to support additional url patterns. Even though the Mvc api allows me to add Endpoints and EndpointDataSources the part that matches the url is internal/sealed. Apart from this the DefaultLinkGenerator is tightly coupled with RouteEndpoint which is also sealed.

In my opinion the Mvc API should change in one of two ways: 1) Make Endpoint and EndpointDatasource internal/sealed to signal that there is no chance of extending endpoint routing OR 2) Make the Matcher part public and decide on what scenarios should be available for customization. Ideally we would like to be able to customise the url matching and generation engine.

We are in the process of moving our CMS to ASP.Net Core and we need to be able to support the url structure and customization options that our platform provided using Mvc 5. In our Mvc 5 implementation we supported url patterns like the following:

{language?}/{category:1,3}/{article:2}

language is optional and can have at most 1 path part category is required and should have between 1 and 3 path parts article is required and should have exactly 2 path parts

Each parameter of the url would have a route constraint that was responsible for: 1) Url generation: Serving as an adapter between the route values and the path part of the url that is being generated. i.e. the constraint could influence what the url would look like. Usually this would involve converting entities into url fragments e.g. article => $"{article.ID}/{article.FriendlyUrl}" This can be achieved in endpoint routing by you need to use two separate classes. IRouteConstraint and IOutboundParameterTransformer. You can not have the same class implement both interfaces because IOutboundParameterTransformer.TransformOutbound is being called twice and this is confusing and will leaded to weird behavior. So the end result is a little clunky.

2) Incoming request: The constraint was responsible for validating that the url fragment is valid and retrieves the entity/model needed to display. So '123/my-article-title' -> id=123 -> db.Articles.Where(x=> x.ID == 123).Select(x=> new MyArticleModel{}) I believe this can be achieved with endpoint routing

pranavkm commented 4 years ago

@rynowak could you respond?

J-ohn commented 4 years ago

So let me answer my own questions: 1) We wanted to customize the Matcher part of the routing engine: This is possible by creating your own middleware. The output of this middleware is in fact an Endpoint (so yeah thats why Endpoint is public). The middleware should look at the HttpContext and decide if it matches or not, produce route values and set an endpoint (using httpContext.SetEndpoint method) whos RequestDelegate actually executes the request. The request delegate can then execute a controller based on the route values, possibly using IActionDescriptorCollectionProvider and IActionInvokerFactory. 2) We wanted to customize url generation: This can be done by decorating LinkGenerator. LinkGenerator has 4 methods which can be overriden and do additional checking to determine the url based on the route values

So in the end, endpoint routing is not customizable in itself but you can create your own collection of endpoints (or routes), and hook them up with a middleware and LinkGenerator so that they work in parallel to it.

davidfowl commented 4 years ago

Have you tried the DynamicRouteValueTransformer?

J-ohn commented 4 years ago

If I got everything right DynamicRouteValueTransformer handles all route values and only for incomming requests. Instead of having a route constraint responsible for a given entity (language, page, article whatever) both for url generation and incomming requests, we would have to have to split the logic of those constraints into two classes: 1) Implementation of IRouteConstraint to check whether we have the route values we need in the route value dictionary during url generation and incoming request. I think IRouteConstraint can actually influence route values in incoming requests so DynamicRouteValueTransformer would not be needed for our scenario 2) IOutboundParameterTransformer to transform the route value to a path

But even then there is always the limitation that a route parameter can not have more than one url segments (i.e. /{category}/{article} => {category} can not have '/' in it)

rynowak commented 4 years ago

Correct: if you want to have complete control over URL matching, write a middleware for this.

The common use case of DynamicRouteValueTransformer is to write a route pattern like "{**slug}" and then run your arbitrary logic to do matching. That will capture the entire URL path for you to process.