MVC Routing Constraint on Controller Names

2019-06-21 06:13发布

问题:

I have routes set up like:

        context.MapRoute(
        name: "Area",
        url: "Area/{controller}/{action}",
        defaults: new
        {
            controller = "Home",
            action = "Dashboard"
        }
        );

        context.MapRoute(
        name: "AccountArea",
        url: "Area/{accountFriendlyId}/{controller}/{action}",
        defaults: new
        {
            controller = "Home",
            action = "Dashboard",
            accountFriendlyId = RouteParameter.Optional
        }
        );

        context.MapRoute(
        name: "AccountCampaignArea",
        url: "Area/{accountFriendlyId}/{campaignFriendlyId}/{controller}/{action}",
        defaults: new
                {
                    controller = "Home", 
                    action = "Dashboard",
                    accountFriendlyId = RouteParameter.Optional,
                    campaignFriendlyId = RouteParameter.Optional
                }
        );

And I have a burning desire to have Area/friendlyAccountName/Home take me to the Dashboard() but this doesn't work (404). I think the reason is that we go looking for a friendlyAccountName controller.

Comfortable with the knowledge that if I should choose to name an account after one of my controllers everything comes crashing down, is there a way to fall through to the next route in case of a string failing to find a corresponding controller? Is there some way to use reflection and avoid maintaining a constraint every time I modify the list of my controllers?

EDIT

Do you know a way that doesn't use reflection or at least contains the derived type search to this area? I don't like the idea of incurring that overhead twice when the second route parameter does match a controller name (pass constraint then search again when constructing controller). I wish there was a way to catch the exception at the point of constructing a controller and then backing up and falling through to the next route.

回答1:

Why do you need the first route at all? If {accountFriendlyId} is optional, you should be able to omit it and get the same route defaults as your first registered route.

That way it would match on the AccountArea named route first, which is what you want, and then if an {accountFriendlyId} isn't specified it would treat the first token after the area as a controller.

In fact, I feel like you should be able remove the first two routes entirely and just stick with the last one, since the first two route parameters are optional and the defaults are identical.

UPDATE

Since the {accountFriendlyId} could be a valid controller operation name, you could do a couple of other things:

  1. Move {accountFriendlyId} to the end of the route, instead of at the beginning. This follows a more natural URL style of broadest resource to specific detail within the resource.
  2. Use route constraints. You could theoretically use reflection to generate the regex to match on controller names in a custom constraint, or you could just write them out manually. Something like this:

context.MapRoute(

    name: "Area",
    url: "Area/{controller}/{action}",
    defaults: new
    {
        controller = "Home",
        action = "Dashboard",
        new { controller = @"(Account|Profile|Maintenance)" }
    }

);



回答2:

Ultimately, to facilitate what I wanted (which was dependent on having the app dynamically distinguish between arbitrary strings and controller names) I set up routes like this:

public override void RegisterArea(AreaRegistrationContext context)
    {
        context.MapRoute(
        name: "AccountCampaignArea",
        url: "Area/{accountFriendlyId}/{campaignFriendlyId}/{controller}/{action}",
        defaults: new
            {
                controller = "Home",
                action = "Dashboard",
                accountFriendlyId = RouteParameter.Optional,
                campaignFriendlyId = RouteParameter.Optional,
                id = UrlParameter.Optional
            },
        constraints: new { accountFriendlyId = new ControllerNameConstraint(), campaignFriendlyId = new ControllerNameConstraint() }
        );

        context.MapRoute(
            name: "AccountArea",
            url: "Area/{accountFriendlyId}/{controller}/{action}",
            defaults: new
                {
                    controller = "Home",
                    action = "Dashboard",
                    accountFriendlyId = RouteParameter.Optional,
                    id = UrlParameter.Optional
                },
            constraints: new { accountFriendlyId = new ControllerNameConstraint() }
            );

        context.MapRoute(
        name: "Area",
        url: "Area/{controller}/{action}",
        defaults: new
            {
                controller = "Home",
                action = "Dashboard"
            }
        );
    }

And set up a constraint like this (the constraint could also be called NotControllerNameContraint):

public class ControllerNameConstraint : IRouteConstraint
{
    private static List<Type> GetSubClasses<T>()
    {
        return Assembly.GetCallingAssembly().GetTypes().Where(
            type => type.IsSubclassOf(typeof(T))).ToList();
    }

    public List<string> GetControllerNames()
    {
        List<string> controllerNames = new List<string>();
        GetSubClasses<Controller>().ForEach(
            type => controllerNames.Add(type.Name));
        return controllerNames;
    }
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.ContainsKey(parameterName))
        {
            string stringValue = values[parameterName] as string;
            return !GetControllerNames().Contains(stringValue + "Controller");
        }

        return true;
    }
}

Credits: https://stackoverflow.com/a/1152735/938472



回答3:

This is a URL space issue. How do you distinguish between an accountFriendlyId, a campaignFriendlyId and a controller ? The easy way is to put them in different segments of the URL, but with your routes a controller can be second, third or forth segment. You have to use constraints to disambiguate, and order them like this:

context.MapRoute(null, "Area/{controller}/{action}",
   new { controller = "Home", action = "Dashboard" },
   new { controller = "Foo|Bar" });

context.MapRoute(null, "Area/{accountFriendlyId}/{controller}/{action}",
   new { controller = "Home", action = "Dashboard" },
   new { controller = "Foo|Bar" });

context.MapRoute(null, "Area/{accountFriendlyId}/{campaignFriendlyId}/{controller}/{action}",
   new { controller = "Home", action = "Dashboard" });

The idea you suggested, if a controller is not found then try the next matching route, it doesn't work that way, once a route matches that's it, you would have to modify the UrlRoutingModule to try to make that work.