Closed rynowak closed 5 years ago
/cc @davidfowl @JamesNK
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.
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.
I mean the routers that want to supply route values and choose actions, and these may want to do link generation too
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.
Make it so that an IEndpointSelectorPolicy can be filter where it applies by candidate set. This is for isolation.
Done for 2.2
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
Quick intro to endpoint routing:
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
.
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:
- A RouteEndpoint has a template, metadata, and a request delegate that will serve the endpoint's response
- 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
- 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
- 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 .
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:
- A RouteEndpoint has a template, metadata, and a request delegate that will serve the endpoint's response
- 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
- 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
- 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 .
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.
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!
@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.
@rynowak, should this be pushed to preview4?
Sure
There's some work happening here in preview 3 - but I don't consider this complete
@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.
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?
@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.
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
@joshua-mng how do you add new routes to the system when new pages are created in the CMS?
@joshua-mng I would be interested to see a more complete sample, do you have a github repo for it?
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.
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.
@davidfowl My project is built for 3.0-preview-4, I tested and it worked. Please give it a try. Thanks!
It’ll be broken in preview6.
@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.
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);
}
}
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:
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
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.
This has been merged for preview 7. Look for more details in the announcement post when preview 7 is available.
👏
tldr; for developers with custom IRouter implementations:
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 removeIRouter
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:
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:
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:
IEndpointSelectorPolicy
can be filter where it applies by candidate set. This is for isolation.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
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