Skip to content

Plan to migrate IRouter based implementations onto Endpoint Routing #4221

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

Closed
rynowak opened this issue Sep 29, 2018 · 32 comments
Closed

Plan to migrate IRouter based implementations onto Endpoint Routing #4221

rynowak opened this issue Sep 29, 2018 · 32 comments
Assignees
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one

Comments

@rynowak
Copy link
Member

rynowak commented Sep 29, 2018

tldr; for developers with custom IRouter implementations:

  • IRouter is currently not compatible with endpoint routing.
  • We will attempt to improve this in 3.0, with new APIs, documentation, or both
  • If you are using a custom IRouter implementation in 2.2 we recommend disabling endpoint routing: services.AddMvc(options => options.EnableEndpointRouting = false)

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:

  • There is no path to support link generation, but I have yet to see an example of this pattern that includes link generation.
  • This does not allow you to reuse HTTP method filtering or other policies. The custom implementation is responsible for handling these concerns.

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 rynowak self-assigned this Sep 29, 2018
@rynowak
Copy link
Member Author

rynowak commented Sep 29, 2018

/cc @davidfowl @JamesNK

@vincent-163
Copy link

vincent-163 commented Oct 5, 2018

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
Copy link
Member

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
Copy link

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

@rynowak
Copy link
Member Author

rynowak commented Oct 6, 2018

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
Copy link
Member Author

rynowak commented Oct 6, 2018

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

Done for 2.2

@natemcmaster natemcmaster transferred this issue from aspnet/Routing Nov 22, 2018
@natemcmaster natemcmaster added this to the 3.0.0-preview1 milestone Nov 22, 2018
@natemcmaster natemcmaster added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Nov 22, 2018
@mkArtakMSFT mkArtakMSFT added enhancement This issue represents an ask for new feature or an enhancement to an existing one feature labels Nov 28, 2018
@jchannon
Copy link
Contributor

jchannon commented Dec 6, 2018

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
Copy link
Member

JamesNK commented Dec 6, 2018

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/uController#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
Copy link
Contributor

jchannon commented Dec 6, 2018 via email

@jchannon
Copy link
Contributor

jchannon commented Dec 6, 2018 via email

@rynowak
Copy link
Member Author

rynowak commented Dec 6, 2018

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.

@rynowak rynowak removed their assignment Dec 12, 2018
@AndrewTriesToCode
Copy link
Contributor

AndrewTriesToCode commented Dec 15, 2018

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!

@rynowak
Copy link
Member Author

rynowak commented Mar 22, 2019

@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
Copy link

joshua-mng commented Mar 29, 2019

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
Copy link

aKzenT commented Mar 29, 2019

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

@dotnetshadow
Copy link

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

@xuanhnvt
Copy link

xuanhnvt commented May 2, 2019

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
Copy link
Member

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
Copy link

xuanhnvt commented May 3, 2019

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

@davidfowl
Copy link
Member

It’ll be broken in preview6.

@rynowak
Copy link
Member Author

rynowak commented May 3, 2019

@xuanhnvt - I'm planning to add something purpose built for your use case. #8955

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

@TheArchitectIO
Copy link

TheArchitectIO commented May 31, 2019

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
Copy link

joshua-mng commented Jun 13, 2019

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
Copy link
Member Author

rynowak commented Jun 16, 2019

I've created a PR for this here: #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 rynowak added Working and removed 1 - Ready labels Jun 16, 2019
@rynowak
Copy link
Member Author

rynowak commented Jun 24, 2019

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

@rynowak rynowak closed this as completed Jun 24, 2019
@rynowak rynowak added Done This issue has been fixed and removed Working labels Jun 24, 2019
@davidfowl
Copy link
Member

👏

@ghost ghost locked as resolved and limited conversation to collaborators Dec 3, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates Done This issue has been fixed enhancement This issue represents an ask for new feature or an enhancement to an existing one
Projects
None yet
Development

No branches or pull requests