MVC Route Attribute error on two different routes
Asked Answered
G

2

2

I'm getting an attribute routing error with MVC 5.2. The error is

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

The routes in question are

[Route("{classname}/{id:int}")]
[Route("Edit/{id:int}")]

The url /Edit/123 throws the error, while the url /someword/123 does not throw the error

Given that Edit/123 is more specific than someword/123 why would it throw an error on /Edit/123?

Thanks,

john

Gallice answered 7/11, 2014 at 17:7 Comment(5)
What happens if classname is Edit? :)Hokusai
I can certainly change the routes. My question is really about understanding why it broke. If the routes are ambiguous because of the design of attribute routing I want to understand why that is, my understanding was that a more specific route will always match. I believe, and I may be wrong about this, that the route /Edit/123 is more specific than /Edit/{id:int}, which is more specific than /{name}/{id:int} .Gallice
Chris, so the error is that there is more than one route that matches /Edit/123? I thought that it would find the precise match and use that, but if it throws routes out until only one is left than I see how that is ambiguous.Gallice
I just went ahead and moved my comment into an answer. Yes, it looks for all possible matching routes, and if it finds more than one, you get an error. Attribute routing is a little more finicky than standard routing because there's no inherent order. In standard routing, it just reads the route config top-down and the first match wins. There's no logical starting point with attribute routing.Memorable
I'm about to try it but maybe a custom route constraint as described here: blogs.msdn.microsoft.com/webdev/2013/10/17/… I agree; this is really awful design.Symmetrize
M
4

The routing framework doesn't make judgments about what route you probably intended (there's actually a route with Edit in it, so obviously I want that one). All it sees is that it has two routes which match the URL it has, and it throws its hands up.

Something like the following should fix the ambiguity, as long as you never need "Edit" as a value for classname:

[Route("{classname:regex(^(?!Edit)$)}/{id:int}")]
Memorable answered 7/11, 2014 at 17:47 Comment(2)
thank you, that clears it up. I remembered the rules according to the old style routing (top down, first match wins)Gallice
Would a custom route constraint as described here achieve the same thing? blogs.msdn.microsoft.com/webdev/2013/10/17/…Symmetrize
S
0

There are two ways to fix this:

A regex constraint, as Chris pointed out.

Or a custom route constraint, like here: https://blogs.msdn.microsoft.com/webdev/2013/10/17/attribute-routing-in-asp-net-mvc-5/

You can create custom route constraints by implementing the IRouteConstraint interface. For example, the following constraint restricts a parameter to set of valid values:

public class ValuesConstraint : IRouteConstraint
{
    private readonly string[] validOptions;
    public ValuesConstraint(string options)
    {
        validOptions = options.Split('|');
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
        }
        return false;
    }
}

The following code shows how to register the constraint:

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

        var constraintsResolver = new DefaultInlineConstraintResolver();

        constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));

        routes.MapMvcAttributeRoutes(constraintsResolver);
    }
}

Now you can apply the constraint in your routes:

public class TemperatureController : Controller
{
    // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin
    [Route("temp/{scale:values(celsius|fahrenheit)}")]
    public ActionResult Show(string scale)
    {
        return Content("scale is " + scale);
    }
}

In my opinion, this isn't great design. There are no judgments about what URL you intended and no specificity rules when matching unless you explicitly set them yourself. But at least you can get your URLs looking the way you want. Hopefully your constraint list isn't too long. If it is, or you don't want to hard-code the route string parameter and its constraints, you could build it programmatically outside the action method and feed it to the Route attribute as a variable.

Symmetrize answered 20/7, 2017 at 5:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.