Problems with creating two routes which won't

2019-05-07 01:03发布

问题:

I'm trying to build my tutorial project with routing. My main objective is to build two routes which won't generate 404 error in any case. By this I mean that if the path is wrong I want routing to use /Home/Index path. I have two following routes -

    routes.MapRoute("Default", "{controller}/{action}", 
                        new {controller = "Home", action = "Index"}
                        );

    routes.MapRoute("Second", "{*catchall}",
                        new {controller = "Home", action = "Index", id = UrlParameter.Optional}
                        );

It works fine when I use nonexistent path which doesn't matches the first route, like this -

But if it does, then I have the following -

or

I understand the reason why it happens. However at the present moment, I only managed to find 'some sort' of a solution. Adding the following code to web.config file -

<customErrors mode="On">
      <error statusCode="404" redirect="~/Home/Index"/>
</customErrors>

However, I don't think that it is the best way to solve this problem. Because, as far as I can understand, it simply catches all errors and redirects it to the correct path, without actually using routing. So I don't think I need this global handling.

So could somebody please give me a hint or provide me with a good solution to my problem. Thank you.

回答1:

Well, you didn't really define what "wrong" routing is, so here is my definition:

  1. The controller or action does not exist in the project.
  2. If an "id" is passed, it must exist on the action method.

Routes

I used constraints to do this. AFAIK, it is not possible to use a constraint on an optional parameter. This means that in order to make the id parameter optional, you need 3 routes.

routes.MapRoute(
   name: "DefaultWithID",
   url: "{controller}/{action}/{id}",
   defaults: new { controller = "Home", action = "Index" },
   constraints: new { action = new ActionExistsConstraint(), id = new ParameterExistsConstraint() }
);

routes.MapRoute(
   name: "Default",
   url: "{controller}/{action}",
   defaults: new { controller = "Home", action = "Index" },
   constraints: new { action = new ActionExistsConstraint() }
);

routes.MapRoute(
    name: "Second",
    url: "{*catchall}",
    defaults: new { controller = "Home", action = "Index" }
);

ActionExistsConstraint

public class ActionExistsConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest)
        {
            var action = values["action"] as string;
            var controller = values["controller"] as string;

            var thisAssembly = this.GetType().Assembly;

            Type[] types = thisAssembly.GetTypes();

            Type type = types.Where(t => t.Name == (controller + "Controller")).SingleOrDefault();

            // Ensure the action method exists
            return type != null && type.GetMethod(action) != null;
        }

        return true;
    }
}

ParameterExistsConstraint

public class ParameterExistsConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest)
        {
            var action = values["action"] as string;
            var controller = values["controller"] as string;

            var thisAssembly = this.GetType().Assembly;

            Type[] types = thisAssembly.GetTypes();

            Type type = types.Where(t => t.Name == (controller + "Controller")).SingleOrDefault();
            var method = type.GetMethod(action);

            if (type != null && method != null)
            {
                // Ensure the parameter exists on the action method
                var param = method.GetParameters().Where(p => p.Name == parameterName).FirstOrDefault();
                return param != null;
            }
            return false;
        }

        return true;
    }
}


回答2:

You can do this by using a custom RouteConstraint.

First, set your routes like this:

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

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
            constraints: new { controller = new ControllerNameConstraint() }

        );

        routes.MapRoute(
            name: "Second",
            url: "{*wildcard}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }

Now you can create the ControllerNameConstraint. I've done a very simple implementation here, you will need to change it so that it works with what you are trying to achieve (you have a bunch of options using reflection if you don't want to update it all the time)

public class ControllerNameConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values[parameterName].ToString() == "Home" || values[parameterName].ToString() == "Account")
        {
            //the route is matched and will run the constrained ("Default") route
            return true;
        }
        //the route is not matched to the constraint and will fall through to your wildcard route
        return false;
    }
}


回答3:

I'm doing something very similar in an attempt to kind of create my own dynamic routing based on a database-controlled site navigation. Essentially, I wanted anything that hit a true defined route to go through the normal routing process, but then I could have URLs for things like content pages controlled entirely by their placement in the navigation. Anyways, for that I relied on the httpErrors Web.config declaration:

<system.webServer>
    ...
    <httpErrors errorMode="Custom" existingResponse="Replace">
        <remove statusCode="404" />
        <error statusCode="404" responseMode="ExecuteURL" path="/error/404" />
    </httpErrors>

Then, I have an ErrorController with an action to process 404s. In that action, I check the attempted URL against the database and if I find a matching item, I hand off the request to the appropriate place. If there's no match, then I just return a view, which is a custom 404 view I set up. The path portion above needs to be the URL to get to your 404 action. Mine is /error/404 because I'm using attribute routing and can make it whatever I want. If you're relying on the default route, you can't have an action named 404, so it would have to be something like /error/http404 or /error/notfound.



回答4:

The way your are specifying your default route is looking for any controller/action paring and if they are not found to substitute with the defaults. If you call out the exact home route and leave out the controller and action keywords in the url when the map is created it will only match on those and all others will be caught by your catch all.

    routes.MapRoute(
        name: "Default",
        url: "Home/Index/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );

I do not know of any other routes you may have so I could not test with your exact scenario. Try this out and see if it helps you.



回答5:

The answer from NightOwl888 is working for me, however the code for ActionExistsConstraint and ParameterExistsConstraint need to modify a little, to remove case-sensitive comparison. Here is my new code and it works perfectly in my case

public class ActionExistsConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest)
        {
            var action = values["action"] as string;
            var controller = values["controller"] as string;

            var thisAssembly = this.GetType().Assembly;

            Type[] types = thisAssembly.GetTypes();

            Type type = types.FirstOrDefault(t => t.Name.Equals(controller + "Controller", StringComparison.OrdinalIgnoreCase));

            // Ensure the action method exists
            return type != null &&
                   type.GetMethods().Any(x => x.Name.Equals(action, StringComparison.OrdinalIgnoreCase));
        }

        return true;
    }
}

public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest)
        {
            var action = values["action"] as string;
            var controller = values["controller"] as string;

            var thisAssembly = this.GetType().Assembly;

            Type[] types = thisAssembly.GetTypes();

            Type type = types.FirstOrDefault(t => t.Name .Equals(controller + "Controller", StringComparison.OrdinalIgnoreCase));
            var method = type.GetMethods().FirstOrDefault(x => x.Name.Equals(action, StringComparison.OrdinalIgnoreCase));

            if (type != null && method != null)
            {
                // Ensure the parameter exists on the action method
                var param = method.GetParameters().FirstOrDefault(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
                return param != null;
            }
            return false;
        }

        return true;
    }


回答6:

Please change route declaration with

 routes.MapRoute("Second", "{*catchall}",
                        new {controller = "Home", action = "Index", id = UrlParameter.Optional}
                        );

routes.MapRoute("Default", "{controller}/{action}", 
                        new {controller = "Home"`enter code here`, action = "Index"}

                    );