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

Plan to migrate IRouter based implementations onto Endpoint Routing #4221

Closed rynowak closed 5 years ago

rynowak commented 5 years ago

tldr; for developers with custom IRouter implementations:


Summary

I'm developing a plan to make it possible for folks implementing IRouter based functionality to migrate endpoint routing. This is our future direction for MVC and the stack in general, and we don't want to leave users/frameworks behind.

I'm also lumping in this category custom IActionSelector implementations. IActionSelector is not used in endpoint routing so it's just as relevant/obsolete for new apps.

Over time we're going to evolve in the direction of endpoint routing being the primary experience for the whole stack. I don't expect that we'd ever be able to delete IRouter or remove IRouter support from MVC. We can offer users more options to migrate and try to make it as enticing as possible, but some portion will be left behind; either by our choice not to add a feature they want, or their choice not to adopt new features and concepts.

Analysis

Based on analysis, I've found 3 major types of usages:

  1. I want to rewrite the route values (usually localization) - example
  2. My route values are in a database (usually a CMS, SimpleCommerce) - example
  3. I am implementing a framework (OData) - example

Note that these are roughly in ascending order of 'off-road-ness' - reusing less of our implementation gradually. Most genuine challenges in framework design related to application authors writing extensibility that uses and is used by the framework.

Let's go case by case and look at these...

I want to rewrite the route values

Usually the case for this is that you want to have one set of actions that you 'multiply' (cross-product) across different localized strings that you want to appear in the URL. There's a major pivot here regarding whether you want to localize the 'route values' or just localize the URL template.

In general all of these things 'just work' with application model today. Some users choose to implement this with IRouter because they have done that in the past or it's what seems most obvious to them, but there's no real problem with using application model for this.

My route values in the database

Generally this kind of system works by defining a slug route in the route table that matches all URLs, and then looking up a set of route values in a database using the URL path or part of it as a key.

Example:

var urlSlug = ... do database query...;
RouteData.Values["controller"] = urlSlug.EntityType.RoutingController;
RouteData.Values["action"] = urlSlug.EntityType.RoutingAction;
RouteData.Values["id"] = urlSlug.EntityId;

This requires a slightly more involved solution, because the route templates can't be defined statically, and because we don't provide a simple way to 'run code' to select an MVC action. My assumption here is that this user doesn't want to access the DB once on startup to create the route table, they want to make configuration changes while the app is running.

The key here is that this user is reusing MVC's action selector by rewriting the route values.


Here's a proposal:

  1. Make it so that an IEndpointSelectorPolicy can be filter where it applies by candidate set. This is for isolation.
  2. Register a single endpoint that with a metadata that identifies it as a 'slug' endpoint
  3. Write an endpoint selector policy that selects the real/action/endpoint and returns it when it finds the 'slug' endpoint

This has a few drawbacks:

Another approach would be to rewrite the URL. That might be more straightforward.

I am implementing a framework

Upon further analysis, this case is very similar to the above. The framework registers a route that matches a 'slug', and has a custom implementation that produces an action.

This case has fewer drawbacks because they are not using MVC's action selection.

Next steps

Make it so that an IEndpointSelectorPolicy can be filter where it applies by candidate set. This is for isolation.

We need to do this. This is a nice perf enhancement to IEndpointSelectorPolicy and can unblock a framework like OData migrate. We need to do this in 2.2 so it's not a breaking change.

We should consider further whether URL rewriting is a better solution for most of these scenarios.

I think the timeframe for any other choices we'd consider is 3.0

rynowak commented 5 years ago

/cc @davidfowl @JamesNK

vincent-163 commented 5 years ago

Is it possible that Endpoint Routing be implemented as a IRouter itself? I personally believe that the original IRouter approach is the only approach that covers all cases. Or at least, a linear-time search through a list of routers, where each router is a blackbox that rewrites or accepts a route, is inevitable. The 'slug' route, or generally any routing that applies to all URLs, apparently does not fit well with Endpoint Routing; Endpoint Routing is like a tree where you traverse from the root and find a leaf, but only one leaf. Even if a url rewriter makes itself an endpoint, it has to travel from the root again, where the endpoint is still on the tree. Also when there are several URL-rewriting mechanisms in place, there must be an order. You don't want every router that rewrites the URL to restart the routing process from the beginning.

davidfowl commented 5 years ago

URL rewriting isn't routing though, why would use use routing to rewrite the URL? We have a URLRewrite middleware specifically for that purpose. You run that before routing makes the decision on what end point gets selected.

vincent-163 commented 5 years ago

I mean the routers that want to supply route values and choose actions, and these may want to do link generation too

rynowak commented 5 years ago

The contract of 'endpoint routing' in 3.0 is to produce an instance of IEndpointFeature and attach it to the http context. https://github.com/aspnet/HttpAbstractions/blob/master/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs

There can be any number of middleware that participate in this cooperatively. There's no reason why you couldn't write an IRouter-based implementation (it's something I've considered). If there's enough demand for us to ship something like this, it's a possibility, but it won't be our default experience.


Link generation is a little more complicated because it's not cooperative - but basically you can choose to implement an 'address scheme' which describes how you find a set of endpoints to attempt to use for link generation.

If you're trying to extend the system, broadly, then maybe the right thing to do is create an 'address scheme' that matches your requirements and implement it.

If you're trying to extend an 'in the box' scheme (like MVC's link generation), the you'll have to replace it and provide an implementation that does what you want.

Generally the kinds of things that lead folks to implement IRouter involve running async code when a request comes in. That's a poor fit for link generation, and it's often ignored, maybe that's ok.


If you really do want to implement routing extensibility then the most reasonable path would be to provide us with all of the endpoints, and we will work it out. If this is something that's impossible for your scenarios, then I'd like to learn more.


My experience helping others understand how to implement routing from WebAPI2/MVC5 -> current is that generally extenders expect a purpose-built extensibility point to exist for what they are trying to do, and shy away from re implementing anything.

Generally in the routing arena it's going to be the case moving forward that you will need to write code if you want to reprogram routing. This is a philosophical pivot on our part to trade openness for performance and features. We think this is the right thing to do because routing is going to be part of just about every scenario.

If you feel like you're blocked, that's not great and we need to make it better.

rynowak commented 5 years ago

Make it so that an IEndpointSelectorPolicy can be filter where it applies by candidate set. This is for isolation.

Done for 2.2

jchannon commented 5 years ago

I've just found this via 2.2 -> Endpoint routing https://blogs.msdn.microsoft.com/webdev/2018/08/27/asp-net-core-2-2-0-preview1-endpoint-routing/

I maintain Carter - https://github.com/CarterCommunity/Carter/tree/master/src and this leverages the routing middleware. Carter registers routes via routeBuilder.MapRoute and then return builder.UseRouter(routeBuilder.Build());

I'd like to know how that will affect Carter if you plan to move away from IRouter. I assume you will still have routing middleware?

The reason for the existence of Carter is because I want the functionality of having the path next to the handler

Get("/blog", async context => await context.Response.WriteAsync("hi"));

If that made it into MVC then that'd be awesome but noone at MS seems interesting in that even though the routing middleware provides that functionality.

If you'd like to work together on trying to move Carter onto the new things you're proposing that'd be awesome, would be beneficial for both parties imo.

Thanks

JamesNK commented 5 years ago

Quick intro to endpoint routing:

  1. A RouteEndpoint has a template, metadata, and a request delegate that will serve the endpoint's response
  2. Endpoint datasources are mapped when the UseEndpointRouting middleware is registered. This middleware will handle matching the template. At the end of the request pipeline a matched endpoint's request delegate is automatically called
  3. If your endpoints are static, e.g. they don't change during the lifetime of the application, then you can map endpoints using MapGet, MapPost, and MapVerbs helper methods
  4. If your endpoints are dynamic, e.g. a new Razor page is added to the app at runtime and needs a new endpoint, then you would create a custom data source

This example of Startup.cs has a mix of using the helper methods and registering MVC endpoints - https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/samples/MvcSandbox/Startup.cs#L35-L70

MVC's endpoint datasource - https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs Custom framework using endpoint routing - https://github.com/davidfowl/Web.Framework/pull/6


It should be fairly simple for you to move from using IRouteBuilder.MapRoute to the Map/MapGet/MapPost/MapVerb/etc extension methods that hang of IEndpointBuilder.

jchannon commented 5 years ago

Thanks.

Guess I need to sit down and spike Carter using endpoint routing. Is that currently possible or should I wait closer to aspnetcore v3 for the apis to be decided as I know there are plans to change it quite a lot from 2.2 to 3 so frameworks can consume them.

On Thu, 6 Dec 2018 at 10:24, James Newton-King notifications@github.com wrote:

Quick intro to endpoint routing:

  1. A RouteEndpoint has a template, metadata, and a request delegate that will serve the endpoint's response
  2. Endpoint datasources are mapped when the UseEndpointRouting middleware is registered. This middleware will handle matching the template. At the end of the request pipeline a matched endpoint's request delegate is automatically called
  3. If your endpoints are static, e.g. they don't change during the lifetime of the application, then you can map endpoints using MapGet, MapPost, and MapVerbs helper methods
  4. If your endpoints are dynamic, e.g. a new Razor page is added to the app at runtime and needs a new endpoint, then you would create a custom data source

This example of Startup.cs has a mix of using the helper methods and registering MVC endpoints - https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/samples/MvcSandbox/Startup.cs#L35-L70

MVC's endpoint datasource - https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs Custom framework using endpoint routing - davidfowl/Web.Framework#6 https://github.com/davidfowl/Web.Framework/pull/6

It should be fairly simple for you to move from using IRouteBuilder.MapRoute to the Map/MapGet/MapPost/MapVerb/etc extension methods that hang of IEndpointBuilder.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/aspnet/AspNetCore/issues/4221#issuecomment-444823335, or mute the thread https://github.com/notifications/unsubscribe-auth/AAGapnOK1I4f0aqvrOrff_FudVTfaW_gks5u2PB2gaJpZM4Yufy4 .

jchannon commented 5 years ago

Out of interest can you see any possibility of having mvc deal with routing like the current routing middleware and carter behaves with the new endpoint routing ie you have a path and handler next to each other?

On Thu, 6 Dec 2018 at 10:38, Jonathan Channon jonathan.channon@gmail.com wrote:

Thanks.

Guess I need to sit down and spike Carter using endpoint routing. Is that currently possible or should I wait closer to aspnetcore v3 for the apis to be decided as I know there are plans to change it quite a lot from 2.2 to 3 so frameworks can consume them.

On Thu, 6 Dec 2018 at 10:24, James Newton-King notifications@github.com wrote:

Quick intro to endpoint routing:

  1. A RouteEndpoint has a template, metadata, and a request delegate that will serve the endpoint's response
  2. Endpoint datasources are mapped when the UseEndpointRouting middleware is registered. This middleware will handle matching the template. At the end of the request pipeline a matched endpoint's request delegate is automatically called
  3. If your endpoints are static, e.g. they don't change during the lifetime of the application, then you can map endpoints using MapGet, MapPost, and MapVerbs helper methods
  4. If your endpoints are dynamic, e.g. a new Razor page is added to the app at runtime and needs a new endpoint, then you would create a custom data source

This example of Startup.cs has a mix of using the helper methods and registering MVC endpoints - https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/samples/MvcSandbox/Startup.cs#L35-L70

MVC's endpoint datasource - https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/MvcEndpointDataSource.cs Custom framework using endpoint routing - davidfowl/Web.Framework#6 https://github.com/davidfowl/Web.Framework/pull/6

It should be fairly simple for you to move from using IRouteBuilder.MapRoute to the Map/MapGet/MapPost/MapVerb/etc extension methods that hang of IEndpointBuilder.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/aspnet/AspNetCore/issues/4221#issuecomment-444823335, or mute the thread https://github.com/notifications/unsubscribe-auth/AAGapnOK1I4f0aqvrOrff_FudVTfaW_gks5u2PB2gaJpZM4Yufy4 .

rynowak commented 5 years ago

Endpoint Routing moves down to a low level a bunch of things that used to only be done in MVC. So if you're authoring a framework, you should find it a lot more powerful.

We'll be revising some of the patterns around startup in 3.0 to make this more first class. Right now (2.2) the ability to interact with routing is still kinda hidden inside MVC.

AndrewTriesToCode commented 5 years ago

I have a multitenant library which supports several ways to detect a tenant. One of them is via a route parameter. This detection happens in middleware prior to UseMVC.

I currently "run" routing in my middleware before the MVC middleware runs. I do this (approximately) by reimplementing RouterMiddleware functionality resulting in RouteContext.RouteData but doesn't set the HttpFeature or call the handler.

It's unclear to me how this new approach will impact my approach. Is there now a better way to get the RouteData prior to the MVC middleware being called?

Thanks!

mkArtakMSFT commented 5 years ago

@davidfowl, please share your thoughts after you'll sync up with @rynowak regarding this. We need to lock down on the approach here, to solve #7011 the right way.

mkArtakMSFT commented 5 years ago

@rynowak, should this be pushed to preview4?

rynowak commented 5 years ago

Sure

rynowak commented 5 years ago

There's some work happening here in preview 3 - but I don't consider this complete

dotnetshadow commented 5 years ago

@JamesNK After communicating with James via Email, I'm adding a comment here on a typical CMS project that would like to convert IRouter to Endpoint Routing. https://github.com/KalikoCMS/KalikoCMS.NextGen/tree/develop

It would be great if there was a way to configure IRouter / Endpoint Routing. Currently IRouter is only used for a section of a website. Keep performance of endpoint routing where possible, fall back to IRouter when necessary.

aKzenT commented 5 years ago

I'm currently trying to move from IRouter to endpoint based routing and encountering some difficulties.

Basically we have some slugs coming from a database and when the slug matches the request we forward it to a specific MVC controller (same action for all slugs). We never used IActionSelector and we don't use a wildcard in the route template. Instead we used a custom RouteCollection which we update whenever the cache is flushed so we can add/remove urls at runtime. We also have some regular MVC routes that are not dynamic and which are registered normally on startup.

At first I thought it would be easy to move to endpoint based routing if I just implement an EndpointDataSource. However I did not find a good way to connect my dynamic endpoints with the controller action.

Looking at the logic needed to create an endpoint for an action here: https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs

it seems quite complex and requires wrapping the request delegate for example. I would rather not copy all this into my own implementation, but unfortunately it seems that all classes that I could use are internal only.

Using MVC to create the endpoints for me doesn't seem to work either as the routes cannot be changed at runtime without implementing a custom IRouter (they are mapped to endpoint once on startup).

So, I'm wondering. Is there any good way for me to use endpoint routing at the moment? And if not, how could this be adressed in 3.0?

rynowak commented 5 years ago

@aKzenT - I plan to enable scenarios like yours as part of the next preview release. I'll update this issue when I have something to share. Thanks for the feedback.

joshua-mng commented 5 years ago

I'm also using IRouter based custom route to change RouteValues (or in other words, intercept routing) and let MVC action selectors select custom action.

This obviously fails in asp.net core 2.2. So I tried to implement this interception using middleware. To test out the prototype, I tried following and it works.

app.UseEndpointRouting();
app.Use(async (context, next) => {
      var selectorContext = context.Features.Get<IEndpointFeature>() as EndpointSelectorContext;
      selectorContext.RouteValues["action"] = "NonExistentAction";

      await next();
 });
app.UseMvc();

You can intercept endpoint routing before MVC action selectors run, and change route values however you want. This obviously doesn't facilitate in link generation, but as explained by @rynowak, that's a hardly a feature we need for this kind of interception, even when using IRouter.

So here it goes for those who are still looking for a way to make IRouter based extensibility work in asp.net core 2.2. By the way, I was also using IRouter based routes in my own custom CMS framework, and also for mapping SLUG urls. And this new middleware based method works just fine.

You can attach special data token in your routes to identify them as needing special handling, and inside your middleware, you can handle accordingly. I can share more complete solution if anyone needs further.

Thanks

aKzenT commented 5 years ago

@joshua-mng how do you add new routes to the system when new pages are created in the CMS?

dotnetshadow commented 5 years ago

@joshua-mng I would be interested to see a more complete sample, do you have a github repo for it?

xuanhnvt commented 5 years ago

Hi all,

I also had an issue with dynamic endpoint routing when change from old routing (using IRouter). I followed @joshua-mng's suggestion, I implemented middleware to redirect enpoint at runtime by changing EndpointSelectorContext, I would like share my project to all who get same issue. Below is project's link: https://github.com/xuanhnvt/GenericEndpointRouting @dotnetshadow If you didn't get any repos, you can refer above link.

Another way we can solve this issue out without using middleware, we can use extension function Map(string pattern, RequestDelegate requestDelegate) that map generic route with generic requestDelegate, its job is extracting genericSlug parameter to slug string, search slug in database, then invoke approriate page action. For details, please refer to below code:

`public static IEndpointConventionBuilder MapFallbackToGenericPage( this IEndpointRouteBuilder endpoints) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); }

        var conventionBuilder = endpoints.Map("{genericSlug}", async context =>
        {
            // get slug value 
            var genericSlug = context.GetRouteValue("genericSlug") as string;
            if (!String.IsNullOrEmpty(genericSlug))
            {
                string pageRouteValue = String.Empty;
                var slugService = context.RequestServices.GetRequiredService<ISlugService>();
                var slug = slugService.GetSlugFromName(genericSlug);
                if (slug != null)
                {
                    switch (slug.Id)
                    {
                        case 1:
                            pageRouteValue = "/Blog/BlogPost";
                            break;
                        case 2:
                            pageRouteValue = "/Blog/BlogCategory";
                            break;
                        default:
                            break;
                    }

                    if (!String.IsNullOrEmpty(pageRouteValue))
                    {
                        // get page action descriptor
                        var actionDescriptors = context.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>().ActionDescriptors;
                        var action = actionDescriptors.Items.OfType<PageActionDescriptor>().Where(item => item.ViewEnginePath.Contains(pageRouteValue)).FirstOrDefault();
                        if (action != null)
                        {
                            // get endpoint context, then custom route values
                            var endpointSelectorContext = context.Features.Get<IEndpointFeature>() as EndpointSelectorContext;
                            endpointSelectorContext.RouteValues["page"] = pageRouteValue;

                            // pass route data to action context
                            var routeData = context.GetRouteData();
                            //var actionContext = new ActionContext(context, routeData, action);

                            // should load compiled page action descriptor into action context, if not (like above) it will produce error
                            var compiledAction = await context.RequestServices.GetRequiredService<PageLoader>().LoadAsync(action);
                            var actionContext = new ActionContext(context, routeData, compiledAction);

                            var invokerFactory = context.RequestServices.GetRequiredService<IActionInvokerFactory>();
                            var invoker = invokerFactory.CreateInvoker(actionContext);
                            await invoker.InvokeAsync();
                        }
                    }
                }
            }
        });
        conventionBuilder.WithDisplayName("GenericEndpoint");
        conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue);
        return conventionBuilder;
    }`

I also added this implementation into above project. I hope this help.

davidfowl commented 5 years ago

Just an FYI this code wiil not work in 3.0, casting the EndpointSelectorContext to IEndpointFeature will fail. @rynowak has been working on a story here and I think we have something that works for 3.0.

xuanhnvt commented 5 years ago

@davidfowl My project is built for 3.0-preview-4, I tested and it worked. Please give it a try. Thanks!

davidfowl commented 5 years ago

It’ll be broken in preview6.

rynowak commented 5 years ago

@xuanhnvt - I'm planning to add something purpose built for your use case. https://github.com/aspnet/AspNetCore/pull/8955

I'd love your feedback on the high-level design.

TheArchitectIO commented 5 years ago

Hi, I have a very similar dilemma. We are trying to write an intermediate router for legacy software. I have to support 2 forms of routing. URL Based where endpoint routing works and a IRouter based one based off a query string parameter. A stripped version of the router we wrote is. i have a lot of legacy software using this Query String parameter and would love to see EndPoint routing enhanced so we can use it instead of the IRouter.


 public class ActionCodeRouter : IRouter
    {
        public IRouter _defaultRouter;

        public ActionCodeRouter(IRouter defaultRouter)
        {
            _defaultRouter = defaultRouter;
        }

        public VirtualPathData GetVirtualPath(VirtualPathContext context)
        {
            return _defaultRouter.GetVirtualPath(context);
        }

        public Task RouteAsync(RouteContext context)
        {
            if (context.HttpContext.Request.Query.ContainsKey("ActionCode"))
            {
                string actionCode = context.HttpContext.Request.Query["ActionCode"].FirstOrDefault() ?? string.Empty;

                //get contoller
                string controller = actionCode.Split('.').First();

                //get action
                string action = actionCode.Split('.').Last();

                RouteData routeData = new RouteData();
                routeData.Values["controller"] = controller;
                routeData.Values["action"] = action;
                context.RouteData = routeData;
            }

            return _defaultRouter.RouteAsync(context);
        }
    }
joshua-mng commented 5 years ago

My proposed workaround above is not working anymore, don't know why.

Although endpoint routing gives more flexibility and performance for other consumers (cors, mvc, and others), it's currently damn hard to extend it. It should be simple enough.

According to people discussing various problems they encounter above, only simple solution we all want is:

  1. Endpoint routing initially selects an endpoint
  2. We inspect routevalues, and transform RouteValues
  3. We want MVC to select action based on the previously transformed RouteValues (or since asp.net 3.0 mvc will not support IActionSelector, we want to reselect new endpoint which matches for our modified route values, like mvc should expose us some method to match an endpoint based on route values)

It's this simple, but it doesn't work anymore. Currently only workaround is to disable endpoint routing, but what happens when version 3 is out soon in fall. Hope we will have better and easier option to implement this in version 3.0

rynowak commented 5 years ago

I've created a PR for this here: https://github.com/aspnet/AspNetCore/pull/8955

It works roughly the way @joshua-mng suggested. If you're interested in this area, please take a look at this and let me know if this works for your needs.

rynowak commented 5 years ago

This has been merged for preview 7. Look for more details in the announcement post when preview 7 is available.

davidfowl commented 5 years ago

👏