Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Allow for routes to be prefixed from outside a controller #2486

Closed
atrauzzi opened this issue May 1, 2015 · 15 comments
Closed

Allow for routes to be prefixed from outside a controller #2486

atrauzzi opened this issue May 1, 2015 · 15 comments

Comments

@atrauzzi
Copy link

atrauzzi commented May 1, 2015

It looks like specifying the route for a controller action decides the route from the root of the app. It would be nice if I could specify in some external location a point off the root path for all the routes in a controller.

So if my action doStuff was mapped to http://myserver/do-stuff, somewhere I could set a prefix for the class that contains the doStuff method that every route defined in it could be nested under my-weird-controller to result in http://myserver/my-weird-controller/do-stuff.

@rynowak
Copy link
Member

rynowak commented May 1, 2015

You can do this by building a convention: https://github.com/aspnet/Entropy/tree/dev/samples/Mvc.CustomRoutingConvention

You can either register the convention using options or turn it into an attribute and put it on your controllers/actions.

@atrauzzi
Copy link
Author

atrauzzi commented May 2, 2015

This seems like a very manual solution, is there no plan to offer a more convenient facility for these kinds of higher level routing configurations?

I still really think there would be merit in studying what Laravel does, especially with routing: http://laravel.com/docs/5.0/routing#route-groups

It's quite brilliant and has a great API (no custom implementations and wiring).

@davidfowl
Copy link
Member

@atrauzzi it would be great if you could come up with an end to end proposal for what you would like to see. Seems like you know laravel and MVC well enough to come up with something that works well in C# or VB.

@atrauzzi
Copy link
Author

atrauzzi commented May 2, 2015

I actually had a similar thought today. I'll see about starting to put together some notes on what I'm encountering as I muck around starting new projets in ASP.NET 5. I definitely catch myself thinking about how certain things would map out in a .NET space (and sometimes if they'd even apply).

@atrauzzi
Copy link
Author

atrauzzi commented May 3, 2015

@davidfowl - I've put together a blog post based on a google doc (link included in post) which hopefully starts to cover in better detail some of the things spoken about here: http://atrauzzi.blogspot.ca/2015/05/routing-dispatch-laravel-5-vs-aspnet.html

I don't necessarily know if it constitutes an end-to-end definition, but I'm willing to make improvements or offer clarficiations based on your suggestions/questions.

@rynowak
Copy link
Member

rynowak commented May 4, 2015

Very cool stuff. I'm currently reading and digesting your post.

Whether or not to provide a higher-level routing experience in the box was a subject of much debate on the team. What we have today is really just building blocks and is conceptually compatible with what a user coming from MVC5 or WebAPI2 would have used.

There's of course potential to define something better, and it's not for a lack of trying on the team that we haven't yet 😆

@atrauzzi
Copy link
Author

atrauzzi commented May 4, 2015

@rynowak - Thanks! Hopefully the examples from Laravel I zeroed in on are enough to convey how nice it is to work with.

If you (or anyone else reading) have questions or want a point clarified/improved, please don't hesitate to ask.

@rynowak
Copy link
Member

rynowak commented May 4, 2015

BTW we do have activation of services in action arguments with [FromService]. It also works on Controller properties as well as properties of DTOs/ViewModels.

@atrauzzi
Copy link
Author

atrauzzi commented May 5, 2015

Awesome, is there somewhere I can see an example of [FromService] being used? Also, out of curiosity, what's the reasoning behind the name "FromService" as opposed to something like "MethodInject" or even just "Inject"?

@rynowak
Copy link
Member

rynowak commented May 5, 2015

You'll find (contrived) examples if you look at the functional tests, my guess is https://github.com/aspnet/Mvc/tree/dev/test/WebSites/ModelBindingWebSite

@atrauzzi
Copy link
Author

atrauzzi commented May 5, 2015

https://github.com/aspnet/Mvc/blob/dev/test/WebSites/ModelBindingWebSite/Controllers/FromServices_CalculatorController.cs#L15

It might be nicer to have a more idiomatic name - something a bit better aligned with the purpose of the feature. I'm sure you're seeing that over at #2151. With the name FromService(s), without this discussion, I might be lead to imply there's more than just dependency injection going on. And to be honest, I might not even infer that the phrase implies dependency injection either! ;)

@danroth27 danroth27 added this to the Backlog milestone May 6, 2015
@glen-84
Copy link

glen-84 commented Jun 4, 2015

In terms of a modular application architecture, where modules such as "forums" and "blogs" are installed via NuGet packages, this type of thing is very important. See also #2551 (comment), dotnet/efcore#2256 (comment), and dotnet/efcore#757 (comment).

Given an MVC web application with the following structure:

My.Website
    wwwroot
    Areas
        Admin
            Controllers
        Site
            Controllers
    Migrations
    Startup.cs
    (etc.)

And a "module" with this structure:

My.NewsModule
    wwwroot
    Areas
        Admin
            Controllers
        News
            Controllers
    ModuleStartup.cs        (?)
    (etc.)

If the news module has routes like this:

/[area]/[controller]/[action]
/[area]/news/[controller]/[action]

We need to be able to adjust the prefix externally (from the host application, for example). I may wish to change "/news" to "/headlines", or "/news" may conflict with another module, or the application itself.

You can assume that modules will be given access to the services container and/or IApplicationBuilder, probably before the application itself (to allow it to override anything done by a module). If you ignore attribute routing for a moment, I'm not even sure how you would use routes.MapRoute from the module without first calling app.UseMvc(), but then it would be called multiple times (I don't know if this has a side-effect).

Also, you have to think about route conflicts. If two modules, or a module and the application, have the same route, then simply extracting routing data from every reachable controller and dumping it in one pile is going to cause issues.

There definitely need to be ways of grouping routes. By area and by assembly would be two ideas. Arbitrary grouping would also be useful (as seen in Laravel).

Ideally, I would love to see two further enhancements:

  1. Strongly-typed routing (you know everyone wants this – think about T4MVC, for example)
  2. Graph-based routing (something along the lines of Superscribe)

The graph may look something like this:

SCHEME      HOST*             PORT    PATH SEGMENTS                                                   QUERY      FRAGMENT
"http"  -> "domain.com"    -> 80   -> "news"  -> {controller}   -> {action}
                           -> 80   -> "admin" -> "news"         -> ArticlesController -> {action}
                                              -> TagsController -> "{id}/{name}"
                                                                -> "something-else"
        -> "sub.domain.com -> 80   -> "test"                                                       -> a=b&c=d -> xyz
"https" -> ...

[*] It may be desirable to break the host up by period (sub -> domain -> com, possibly in reverse, depending on the use cases).

Areas should probably get a simple class, like class News : Area or an attribute like [Area(name="x")] on a simple class. This would allow the area to be strongly typed as well. I know that there used to be an AreaRegistration class or something like that, but I don't know if it still exists. Placing an Area class in each area would also save you from having to annotate every controller in the area.

You would define the route like this (it could also be constructed using attributes):

routes.Define(
    scheme: RouteScheme.HTTPS,                             (default = any)
    host: "domain.com" or "{something}.domain.com",        (default = any)
    port: 443                                              (default = any)
    path: "news/{controller}/{action}"
        OR
    path: new [] {
        "news",
        "{controller}" OR ArticlesController (typeof?),
        "{action}" OR nameof(ArticlesController.Index)
    }
    // Possibly query and fragment params, with options to ignore query param order.
    // The HTTP method might also be introduced before the scheme, allowing you to use a
    // single attribute like [Route("x", Method = "GET")] (instead of HttpGet as well)
    // and branching at the method level (i.e. if it's a GET request, don't look at any
    // POST routes). The default would be any method.
)

OR, something like this:

var schemeNode = routes.DefineNode(scheme: RouteScheme.HTTPS);

var hostNode1 = routes.DefineNode(host: "domain.com", schemeNode);
var hostNode2 = routes.DefineNode(host: "{something}.domain.com", schemeNode);

// If no path nodes, port nodes, etc. are included, they receive the default values.
routes.AddNodes(hostNode1, hostNode2);

This is just a made up syntax, but the general idea is to build up a route based on the individual parts (nodes), similar to what Superscribe does here, but without the DSL.

Each branch would be sorted for efficient lookup, with literal strings at the top, and generic {controller}-type patterns at the bottom. When the request is received, the URL is split into each part (scheme, host, port, etc.) and the path is also split on forward slashes. The algorithm then attempts to match each part as it walks up the tree. If the scheme is HTTP, and the tree includes both HTTP and HTTPS, then the HTTPS branch is never even looked at, and the same is true for the other nodes (path segments, etc.).

Node data can also be gathered from attributes in the current assembly (but this should be done explicitly). The node tree from a module in a separate assembly can be merged with the tree in the host application, with a delegate to resolve conflicts (the delegate has access to the nodes and can introduce a new path segment or overwrite an existing one to correct conflicts or customize the tree. It could even switch an entire tree to HTTPS by modifying the scheme node at the root).

This all seems quite complicated, but what is really needed is:

  1. Strongly-typed routes. (no magic strings, easier refactoring)
  2. Graph-based routes. (efficient, flexible)
  3. More control over when routes are added from each assembly, conflict resolution, and customization.

@atrauzzi
Copy link
Author

Great notes @glen-84! Totally agree with what you said, and I feel like the current options (which I know are holdovers from previous versions) aren't as useful as the explicit route definitions we see in frameworks from other languages.

I think the community as a whole has realized that coupling route definitions with the controllers ends up being quite sticky. I'm really hopeful that ASP.NET can take on these free lessons-learned.

@Tessalator
Copy link

@atrauzzi can you point me to some of the discussions about the "whole community .. coupling routes ... controllers". I'm not sure what this is about, but it sounds like moving away from Attribute Routing. If so...

I do most of my work as a business/technology architect where I design business processes and technology services (usually at the service/interface/"method" level), and map the two. A primary goal in this position is to ensure clean decoupling of the business/technology[and implementation] boundary. To do this I shouldn't (theoretically) be aware of controller/action being the technology implementation, though I do need to define that technology interface. In reality I am aware of the implementation and named routes on controller methods are very helpful and sort of a middle ground. I don't care which controllers implement the methods, only that there is a method on the route. By not architecting using controller/action the software architect is able to write more "code centric" clearer code against my business function requirements. Consider this (very long for illustration) route-attributed action:

[This is in a ViewController]
....
[Route("domain/BusinessArea/BusinessFunction/ApplicationService/ServiceFunction/{serviceParameter}"), name="domain.businessarea.businessfunction.applicationservice.servicefunction"] 
[ViewPath("~/ViewLibrary/View.cshtml")]
[LayoutPath("~/LayoutLibrary/_Layout.cshtml")]
public ViewResult ProcessServiceFunctionXXX(string serviceParameter){
   .. Confirmation confirmation = Do something like a save and get a confirmation object
   return View(Attributes.ViewPath, Attributes.LayoutPath, confirmation);
}

Or in a fully route driven approach

[This is in a DomainController]
....
[Route("domain/BusinessArea/BusinessFunction/ApplicationService/ServiceFunction/{serviceParameter}"), name="domain.businessarea.businessfunction.applicationservice.servicefunction"] 
public RedirectToRouteResult SomeNameThatIsMeaningfulForImplementation( ) {
    ...BusinessLogic-> BusinessModel
or
    .. Confirmation confirmation = Worker.WorkFunction(serviceParameter)
    return RedirectToRoute( "Activities.Planning.Tree.ActivityPlanningViews.ShowPlanningTreeView", BusinessModel (or confirmation model) );
}

This isn't actually correct as RedirectToRoute doesn't support a model (why?) with model is passed in ViewData, and View no longer supports an overload that accepts a layout (why?). The dot notation in the route name lets me create "business namespaces". With this approach the method name becomes irrelevant and can better reflect what the code is doing. (I realize that breaking the ViewName = ActionName will be considered heresy by many :)). "SomeNameThatIsMeaningfulForImplementation" is more meaningful to a most programmers working on all but the most front-end View components. It provides better "separation of team concerns" as the route can become the top of a requirement-set for a development team that doesn't need to know about a page or how someone ended up (was routed) there.

I can also program my business workflows treating the routes as BusinessMethods. Very useful if you are orchestrating your workflows with a workflow engine. Using the dot notation on my route names lets me create trees of functionality. In this example I am actually using a dual controller approach. The first is a domain controller (DomainController : Controller) and the second a view controller (ViewController : Controller). This gives two ways to work with the domain. If I am invoking a business function I route to the DomainController (not knowing the controller, just a route), which does the work and sends the results to a ViewControler that manages the display. If I am doing something simple like a direct CRUD function I hit the ViewController and let do the save, again not aware of a controller, just a route.

The bottom line on taking routes out of controllers/actions (If I understood correctly) is that I feel there should be more movement towards attribute routing. As an business/technology architect I can develop entire application architectures and workflows based on these routes only. The developer can implement anyway they want. The developer can also easily move things around easily, because this provides better encapsulation. If you keep a route/action approach everything is encapsulated and portable - I can cut a RouteMethod out of one controller and paste it in another and noting breaks. There is no need to change any route tables etc.

NOTE: This comment is getting off topic I think, and I likely missed the initial comment point.. I'm new to this group and don't know how best to submit this type of information. I have an "Attribute Driven" architecture I use extensively that is based on (guess what) Attributes, and it breaks severely in 6. I find attributes really powerful since they are design time if you precompile, giving flexibility and performance benefits. I would like to share this architecture and get comments on if things should be put into 6 or if there is a way to do these things already that I am not aware of. Let me know if this is too far off topic and I will remove it or move it.

@Eilon
Copy link
Member

Eilon commented Jun 9, 2017

We are closing this issue because no further action is planned for this issue. If you still have any issues or questions, please log a new issue with any additional details that you have.

@Eilon Eilon closed this as completed Jun 9, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants