The current implementation of MVC attribute routing doesn't play well with action selection (different from how attribute routing works in Web API). Specifically, ActionMethodSelector supports ActionMethodSelectorAttribute and ActionNameSelectorAttribute, which don't work together with attribute routing.
Some example scenarios follow.
Versioning:
public class HomeController : Controller
{
[Version1]
[ActionName("Index")]
public ActionResult IndexV1()
{
return Content("Old");
}
[Version2]
[ActionName("Index")]
public ActionResult IndexV2()
{
return Content("New");
}
}
public class Version1Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "1" || String.IsNullOrEmpty(version);
}
}
public class Version2Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "2";
}
}
Using attribute routing:
[RoutePrefix("Home")]
public class HomeController : Controller
{
[Version1]
[Route("Index")]
public ActionResult IndexV1()
{
return Content("Old");
}
[Version2]
[Route("Index")]
public ActionResult IndexV2()
{
return Content("New");
}
}
public class Version1Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "1" || String.IsNullOrEmpty(version);
}
}
public class Version2Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "2";
}
}
Verbs:
Using traditional routing:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
public class HomeController : Controller
{
[HttpGet]
[ActionName("Index")]
public ActionResult GetIndex()
{
return Content("OK; I GET that.");
}
[HttpPost]
[ActionName("Index")]
public ActionResult PostIndex()
{
return Content("POST says beep.");
}
}
Using attribute routing:
[RoutePrefix("Home")]
public class HomeController : Controller
{
[HttpGet]
[Route("Index")]
public ActionResult GetIndex()
{
return Content("OK; I GET that.");
}
[HttpPost]
[Route("Index")]
public ActionResult PostIndex()
{
return Content("POST says beep.");
}
The general problem here revolves around ActionMethodSelector.FindActionMethods, which uses ActionMethodSelectorAttribute as well as ActionNameSelectorAttribute, both of which are public extensibility points and allow controlling MVC’s action selection (basically parallel to the problems we ran into in Web API with decisions being split between routing and action selection).
Some example scenarios follow.
Versioning:
public class HomeController : Controller
{
[Version1]
[ActionName("Index")]
public ActionResult IndexV1()
{
return Content("Old");
}
[Version2]
[ActionName("Index")]
public ActionResult IndexV2()
{
return Content("New");
}
}
public class Version1Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "1" || String.IsNullOrEmpty(version);
}
}
public class Version2Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "2";
}
}
Using attribute routing:
[RoutePrefix("Home")]
public class HomeController : Controller
{
[Version1]
[Route("Index")]
public ActionResult IndexV1()
{
return Content("Old");
}
[Version2]
[Route("Index")]
public ActionResult IndexV2()
{
return Content("New");
}
}
public class Version1Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "1" || String.IsNullOrEmpty(version);
}
}
public class Version2Attribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
string version = controllerContext.RequestContext.HttpContext.Request.Headers["Version"];
return version == "2";
}
}
Verbs:
Using traditional routing:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
public class HomeController : Controller
{
[HttpGet]
[ActionName("Index")]
public ActionResult GetIndex()
{
return Content("OK; I GET that.");
}
[HttpPost]
[ActionName("Index")]
public ActionResult PostIndex()
{
return Content("POST says beep.");
}
}
Using attribute routing:
[RoutePrefix("Home")]
public class HomeController : Controller
{
[HttpGet]
[Route("Index")]
public ActionResult GetIndex()
{
return Content("OK; I GET that.");
}
[HttpPost]
[Route("Index")]
public ActionResult PostIndex()
{
return Content("POST says beep.");
}
The general problem here revolves around ActionMethodSelector.FindActionMethods, which uses ActionMethodSelectorAttribute as well as ActionNameSelectorAttribute, both of which are public extensibility points and allow controlling MVC’s action selection (basically parallel to the problems we ran into in Web API with decisions being split between routing and action selection).