Question

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)
    {
    }
}
Was it helpful?

Solution 2

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.

OTHER TIPS

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...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top