2
votes

I am having an issue with ambiguous routes when using attribute routing. The problems stem from (I believe) using variable parameters at the root of our routes. What's vexing me is that literal parameters do not seem to be taking precedence, and MVC5 can't determine which route to use.

I've run into this before with other routes and thought I'd managed a fix by defining a convention route. With that in mind, where can I find more information on best practices for attribute routing, and resolving ambiguities?

Here's the code I'm having trouble with, as well as the exception.

Server Error in '/' Application.

Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.

The request has found the following matching controller types:

AccountController

RoundController

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidOperationException: Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.

The request has found the following matching controller types:

AccountController

RoundController

RouteConfig.cs

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    // I added this constraint resolver to resolve some other ambiguous routes that end
    // with a literal, but MVC5 wasn't able to determine whether to use those routes
    var constraintResolver = new System.Web.Mvc.Routing.DefaultInlineConstraintResolver();
    constraintResolver.ConstraintMap.Add("notWriteAction", typeof(IsNotWriteActionConstraint));

    routes.MapMvcAttributeRoutes(constraintResolver);

    // This is the convention route I added to resolve another ambiguous route.
    routes.MapRoute(
        name: "Account",
        url: "Account/{action}/{GroupName}/{AccessToken}",
        defaults: new { controller = "Account", action = "Login", GroupName = UrlParameter.Optional, AccessToken = UrlParameter.Optional }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

RoundController.cs

public class RoundController : ControllerBase
{
    [Route("{groupid}/{campaignid}/{accesstoken}")]
    public async Task<ActionResult> TempRoundLink(string groupid, string campaignid, string accesstoken)
    {
    }
}

AccountController.cs

public class AccountController : ControllerBase
{
    [AllowAnonymous]
    [Route("Account/ResetPassword/{token}")]
    public async Task<ActionResult> ResetPassword(string token)
    {
    }
}
2
Having the same trouble. I was able to resolve it by adding a negating regex contraint, but I don't think it's right. It will quickly get out of hand and become difficult to maintain.Josh Wheelock
Hi @JoshWheelock, I need to apologize - I intended to write up the solution I found. I also went with a constraint to start, but abandoned that for something more maintainable. I ended up stripping out all the Microsoft attribute routing we were using, and went with "Attribute Routing," which turns out to be an entirely different package. attributerouting.net It's been working a treat!aholmes
Thank you for the updated link!aholmes

2 Answers

1
votes

I know this isn't quite what you're asking, but I think this can be fixed by using RoutePrefix attribute. E.g.

[RoutePrefix("Round")]
public class RoundController : ControllerBase
{
    [Route("{groupid}/{campaignid}/{accesstoken}")]
    public async Task<ActionResult> TempRoundLink(string groupid, string campaignid, string accesstoken)
    {
    }
}

[RoutePrefix("Account")]
public class AccountController : ControllerBase
{
    [AllowAnonymous]
    [Route("Account/ResetPassword/{token}")]
    public async Task<ActionResult> ResetPassword(string token)
    {
    }
}

I think the reason it tries not to make an assumption is because preference may not be always ascertainable. E.g. how would you expect router to figure these two out:

Routes (from attributes):
"Account/ResetPassword/{token}"
"Account/{something}/alpha"

Reequest:
"/Account/ResetPassword/alpha"

but that's just a guess...

1
votes

I intended to write up something much longer than this, but I haven't had the time. The solution I ended up going with was to strip out any calls to Microsoft's attribute routing library, and to instead use the "Attribute Routing" package found here. http://htmlpreview.github.io/?https://github.com/mccalltd/AttributeRouting/blob/gh-pages/index.html

It's been working very well, and the *Precedence properties resolve the exact issue I was having in my original question.