I have a super simple controller with 2 methods:
public IActionResult Users(long id)
{
return Json(new { name = "Example User" });
}
public IActionResult Users()
{
return Json(new { list = new List<User>() });
}
One to select all users and the other to return all users. In web api 2 I could user the following route and everything worked fine:
config.Routes.MapHttpRoute(
name: "Users",
routeTemplate: "v1/Users",
defaults: new { action = "Users", controller = "Users" },
constraints: null,
handler: new TokenValidationHandler() { InnerHandler = new HttpControllerDispatcher(config) }
);
I have the following routes setup in startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute(name: "User_Default", template: "v1/{controller=Users}/{action=Users}/{id?}");
});
However this gives me a AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied
What am I doing wrong?
In your original webapi code, you were using Routes.MapHttpRoute
which adds webapi specific routes. This is different from an MVC route which won´t take into account the parameters in the action, for instance you would have the same problem in MVC 5 if you were using Routes.MapRoute
.
The same thing is happening in your MVC 6 code, since you are adding a standard MVC route using routes.MapRoute
. In both cases the framework is finding 2 controller actions matching the same route with no additional constraints. It needs some help in order to select one of those 2 actions.
The easiest way to disambiguate the api actions would be using attribute routing instead of defining a route, as in this example:
[Route("v1/[controller]")]
public class UsersController : Controller
{
[HttpGet("{id:int}")]
public IActionResult Users(long id)
{
return Json(new { name = "Example User" });
}
public IActionResult Users()
{
return Json(new { list = new[] { "a", "b" } });
}
}
There are other options that would let you change the behaviour of the MVC routing in MVC 6. You could create your own IActionConstraint
attribute to enforce having or not a given parameter. That way one of those actions requires an id parameter in the route while the other requires not to have an id parameter (Warning, untested code):
public class UsersController : Controller
{
[RouteParameterConstraint("id", ShouldAppear=true)]
public IActionResult Users(long id)
{
return Json(new { name = "Example User" });
}
[RouteParameterConstraint("id", ShouldNotAppear=true)]
public IActionResult Users()
{
return Json(new { list = new[] { "a", "b" } });
}
}
public class RouteParameterConstraintAttribute : Attribute, IActionConstraint
{
private routeParameterName;
public RouteParameterConstraintAttribute(string routeParameterName)
{
this.routerParamterName = routerParameterName;
}
public int Order => 0;
public bool ShouldAppear {get; set;}
public bool ShouldNotAppear {get; set;}
public bool Accept(ActionConstraintContext context)
{
if(ShouldAppear) return context.RouteContext.RouteData.Values["country"] != null;
if(ShouldNotAppear) return context.RouteContext.RouteData.Values["country"] == null;
return true;
}
}
A better option to deal with webapi 2 style controllers would be adding conventions into the MVC pipeline. This is exactly what the Microsoft.AspNet.Mvc.WebApiCompatShim
is doing to help migrating webapi 2 controllers. You can see the conventions added here. Check this guide for a quick overview of this package.