Question

I am using AttributeRouting: http://attributerouting.net/

If I have two actions with the same name, but the route for the GET is different than the route for the POST, why is Url.Action generating a URL that matches on my GET action, even though my action method is pointing to the one that is a post?

I have tried passing the verb as an

Url.Action("CreateEdit", new { method = "POST" })

However this returns the string /enrollments/create instead of /enrollments/createedit which corresponds to the POST.

I wouldn't expect this to work but tried it as some route configurations use this technique:

Url.Action("CreateEdit", new { HttpMethodConstraint = new HttpMethodConstraint("POST") })

However both of these don't solve the problem, and the URL is still for my GET route.

I have verified that I can post to a hardcoded URL of /enrollments/createedit so the POST action does indeed have that route, just to rule out the possibility that the POST Action's default [POST] is defaulting to the expected route.

Problem is that I usually avoid hardcoded URL's and use Url.Action or Html.BeginForm (which also exhibits the same issue) so that all URLs are derived from action routes.

Here are how I have defined my actions:

[GET("create")]
[GET("edit/{id:guid?}")]      
public ActionResult CreateEdit(Guid? id)
...

[POST] //default to "createedit"
public ActionResult CreateEdit()
...

Note this question is not about making the actual request, but is generating route urls.

How do you indicate to Url.Action that it should use the route from the [POST] instead of the GET. There is no overload that take constraints or HTTP Verb? Note that Html.BeginForm exhibits that same issue when generating the action URL attribute, which is clearly a POST oriented method.

It also does not work if I simplify my action routes to(although the above are my goal routes):

[GET("create")]
public ActionResult CreateEdit()
...

[POST("createedit")] //default to "createedit"
public ActionResult CreateEdit()
...

Url.Action("CreateEdit", new { method = "POST" }) returns "/enrollments/create" obviously because method = 'POST' is not the correct way to specify the desired route for Url.Action. Is there a way to tell Url.Action or Html.BeginForm to specify that the POST route is desired? The method param of BeginForm effects the method="post" html attribute, but the action attribute is still generated with the wrong /enrollments/create URL. I've seen area="Admin" used to specify Areas in route parameters. Is there some magic property that can be added to specify the verb? If there is not, then clearly there is no way to get the right URL consistently and I will probably need to rename one of my actions if I need to maintain the desired routes. My hope was there was some route value I could add analogous to new { area = "SomeArea" }. method = "POST" was just my wild guess at what might work.

The request pipeline respects route constraints, if they are both the same route, then it works fine. So if both were [POST("creatededit")] and [GET("create"] then it would work fine because the URLs for both are the same, and then when the actual request is made the distinction is made by the pipeline due to the HttpMethodContraint.

If I use parameters to make the routes distinct, it works but only if I eliminate the [GET("create")]

If Url.Action has no known/supported/documented way to take a route property then the answer may simply be that Url.Action doesn't have any way for you to tell it the desired VERB.

  • If someone asks: Is there a way to specify the area for Url.Action?
  • The answer is simply: Url.Action("SomeAction", new { area ="SomeArea" })
  • The answer is not: muck around with your routes so you don't have two actions of the same name in different areas(or any number of other things that don't address the question).
  • The answer is not: You shouldn't have actions of the same name in different areas. (It might be a valid point, but it doesn't create a useful resource for others who are in a situation where they absolutely must generate a URL for a different area).

I don't understand what is so complicated about that.

I'm not asking how to make a POST request. I know how to do that, but I don't intend to hardcode URLs into my views. That turns into a maintenance nightmare.

I could change my routes if my boss wasn't so set on SOE. So I can't eliminate the [GET("create")] route.

I can resolve by renaming an action like so:

[GET("create")]
[GET("edit/{id:guid?}")]      
public ActionResult CreateEdit(Guid? id)
...

[POST("createedit")] 
public ActionResult CreateEditPost()
...

That is a bit dirty, but works. It still doesn't answer the question.

If there is a way to specify the VERB for Url.Action, I think the answer would be a useful resource. Maybe not to as many people as Url.Action("SomeAction", new { area ="SomeArea" }) but it would be documenting a feature of Url.Action that for me has been elusive. Maybe it doesn't support it. Perhaps I will need to delve into the source code.

Was it helpful?

Solution

I goofed in my original answer. I wasn't thinking it through really and probably answered too quickly. Here's what it boils down to:

Url.Action is tied pretty inherently to the controller/action style routing. When there's two matching actions (overloads where one is a GET version and the other is a POST), the URL should be the same, and so the GET or really first version is returned. See, AttributeRouting just sort of layers on by letting you customize the outward facing URL, but internally, Url.Action is simply trying to find a route that will get you to the requested action. Once it finds a match, it assumes that's good enough, and especially pre-MVC5, it should have been.

MVC5 introduced attribute routing as a first-class citizen, but from what I've seen, this edge case (where the GET and POST versions of the same action have different URLs and you want the POST version in particular) has not been covered.

That said, I see a couple of potential workarounds:

  1. Use different action names. If your POST action is named something like CreateEditPost, then you can very easily do Url.Action("CreateEditPost") and get the right URL. Since your routes are not affected directly by the action name, it doesn't really matter what it's called.

  2. Use named routes. If you name your routes, then you can use Url.RouteUrl instead and request exactly the route your want. For example:

    [POST("createedit", RouteName = "CreateEditPost")]
    public ActionResult CreateEdit()
    {
        ...
    }
    

    Then:

    @Url.RouteUrl("CreateEditPost")
    
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top