Using Both of Attribute and Convention Routing

2019-07-19 03:19发布

问题:

Is there a way to use Convention and Attribute Routing together? I want to call an action method with the real name of method and controller when I defined the attribute routing.

The mapping method is calling at startup:

public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapMvcAttributeRoutes();///
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }

Here is the controller:

[RoutePrefix("d")]
[Route("{action=index}")]
public class DefaultController : Controller
{
    [Route]
    public ActionResult Index()
    {
        return View();
    }

    [Route("f")]
    public ActionResult Foo()
    {
        return View();
    }
}

I can reach to Foo action method with /d/f url. But when I try this url: /Default/Foo, the 404 error occurs. Actually it throws the action not found exception which it says like A public action method 'Foo' was not found on controller 'Namespace...DefaultController'.

I checked the source code of asp.net mvc and I saw these lines:

if (controllerContext.RouteData.HasDirectRouteMatch())
{
    ////////
}
else
{
    ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
    return actionDescriptor;
}

It checks if there is a direct route or not, which the /Default/Foo route is not a direct route so it should act as convention routing which is registered at startup as {controller}/{action}/{id}. But it doesn't find the action with controllerDescriptor.FindAction method and it throws the exception.

Is this a bug or cant I use both routing methods together? Or are there any workaround to use both?

Edit

I debugged into mvc source code, and I saw these lines:

namespace System.Web.Mvc
{
    // Common base class for Async and Sync action selectors
    internal abstract class ActionMethodSelectorBase
    {
        private StandardRouteActionMethodCache _standardRouteCache;

        protected void Initialize(Type controllerType)
        {
            ControllerType = controllerType;

            var allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
            ActionMethods = Array.FindAll(allMethods, IsValidActionMethod);

            // The attribute routing mapper will remove methods from this set as they are mapped.
            // The lookup tables are initialized lazily to ensure that direct routing's changes are respected.
            StandardRouteMethods = new HashSet<MethodInfo>(ActionMethods);
        }

The last comments about attribute routing explains why this problem happens. The Attribute routing removes StandardRouteMethods when you call MapMvcAttributeRoutes.

I'm still seeking a workaround.

回答1:

I want to call an action method with the real name of method and controller when I defined the attribute routing.

It seems like you are going pretty far in the wrong direction if all you want is to call the application knowing the controller and action names. If you have the name of the controller and action (area and other route values) you can use them to get the URL to use quite easily.

var url = Url.Action("Index", "Default");
// returns /d/f

If this works for your usage, then you don't have to have a duplicate set of route mappings at all.

NOTE: Creating 2 URLs to the same page is not SEO friendly (unless you use the canonical tag). In fact, many question here on SO are about removing the duplicate routes from the route table. It seems like they did us all a favor by removing the duplicate routes so we don't have to ignore them all manually.

Alternatives

One possible workaround is to add 2 route attributes to the action method (I don't believe it can be done without removing the RoutePrefix from the controller).

[Route("d/f")]
[Route("Default/Foo")]
public ActionResult Foo()
{
    return View();
}

Another possible workaround - don't use attribute routing for all or part of your application. Attribute routing only supports a subset of convention-based routing features, but is useful in some specific scenarios. This does not appear to be one of those scenarios.