Search code examples
c#asp.net-mvcasp.net-mvc-5asp.net-mvc-routingattributerouting

Url.Action result does not resolve using Route attribute


I'm rebuilding the front-end of an application and due to its complexity I have to work on the existing legacy business layer. As a result, we have things called "News" and "Documents" but in reality both are "Documents" where it is stored.

I've made a DocumentsController which handles everything just fine, slapping a [Route("News/{action=index}")] and [Route("Documents/{action=index}")] on the controller allows me to refer to the controller as either News or Documents. So far so good. Viewing a specific Document using a single ActionResult with the attributes [Route("Documents/View/{id}"] and [Route("News/View/{id}"] also works just fine. However I am running into an issue when I try to use anything other than id as the parameter but only for the News part.

My ActionResult method has the following definition

[Route("Documents/Download/{documentGuid}/{attachmentGuid}")]
[Route("News/Download/{documentGuid}/{attachmentGuid}")]
public ActionResult Download(Guid documentGuid, Guid attachmentGuid)
...

And my View has the following to get the link

<a href="@Url.Action("Download", "Documents", new { documentGuid = Model.Id, attachmentGuid = attachment.AttachmentId })">Download</a>

This will generate a link akin to site/Documents/Download/guid/guid perfectly whenever I have "Documents" as the controller, but if I put "News" there I get a URL generated that uses querystring akin to site/News/Download?guid&guid parameters and resolves into a 404. If I then manually remove the querystring tokens and format the URL manually it will resolve fine.

What is going wrong here, is something conflicting that I am missing?


Solution

  • When looking up a route on an incoming request, routing will use the URL to determine which route matches. Your incoming URLs are unique, so it works fine.

    However, when looking up a route to generate, MVC will use the route values to determine which route matches. The literal segments in the URL (News/Download/) are completely ignored for this part of the process.

    When using attribute routing, the route values are derived from the controller name and action name of the method you have decorated. So, in both cases, your route values are:

    | Key             | Value           |
    |-----------------|-----------------|
    | controller      | Documents       |
    | action          | Download        |
    | documentGuid    | <some GUID>     |
    | attachmentGuid  | <some GUID>     |
    

    In other words, your route values are not unique. Therefore, the first match in the route table always wins.

    To get around this problem, you can use named routes.

    [Route("Documents/Download/{documentGuid}/{attachmentGuid}", Name = "Documents")]
    [Route("News/Download/{documentGuid}/{attachmentGuid}", Name = "News")]
    public ActionResult Download(Guid documentGuid, Guid attachmentGuid)
    ...
    

    Then, resolve the URLs with @Url.RouteUrl or @Html.RouteLink.

    @Html.RouteLink("Download", "News", new { controller = "Documents", action = "Download", documentGuid = Model.Id, attachmentGuid = attachment.AttachmentId })
    

    Or

    <a href="@Url.RouteUrl("News", new { controller = "Documents", action = "Download", documentGuid = Model.Id, attachmentGuid = attachment.AttachmentId })">Download</a>