Route patterns vs individual routes

[亡魂溺海] 提交于 2019-12-10 11:05:48

问题


Currently I have a controller which looks something like this:

public class MyController : Controller
{
    public ActionResult Action1 (int id1, int id2)
    {

    }

    public ActionResult Action2 (int id3, int id4)
    {

    }
}

As you can see both my controllers have the same parameter "pattern", two non-nullable signed integers.

My route config looks like this:

routes.MapRoute(
    name: "Action2",
    url: "My/Action2/{id3}-{id4}",
    defaults: new { controller = "My", action = "Action2", id3 = 0, id4 = 0 }
);

routes.MapRoute(
    name: "Action1",
    url: "My/Action1/{id1}-{id2}",
    defaults: new { controller = "My", action = "Action1", id1 = 0, id2 = 0 }
);

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

However, I have a lot more controller actions than just these two, so basically I've been mapping a separate route for each one, which strikes me as pretty messy.

What I'm wondering is whether I can do something like the default route with two parameters instead of one, such as:

routes.MapRoute(
    name: "Default2",
    url: "{controller}/{action}/{id1}-{id2}",
    defaults: new { controller = "Home", action = "Index", id1 = 0, id2 = 0 }
);

However, given that my parameters are not always named id1 and id2 this won't work ( Error is "The parameters dictionary contains a null entry for parameter". Is there a way I can do this? (Or a completely different way which is better?)

Thanks for the help!

EDIT: Based on the answers thus far it seems my question was a little misleading. I'm wanting a route with generic parameters in particular, so not tied to a specific parameter name.

I want to be able to tell the route manager that "Hey, if you get a request with the pattern {controller}/{action}/{parameter}-{parameter}, I want you to pass those two parameters to the controller and action specified regardless of their type or name!


回答1:


Just look at the default route:

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

The key bit is the UrlParameter.Optional. That allows you to serve actions that look like:

public ActionResult Action1()

And:

public ActionResult Action2(int id)

Or even:

public ActionResult Action3(int? id)

(The latter allows a null value for the parameter)

Obligatory Warning

Keep in mind that there's a few caveats with having generic routes. The model binder in MVC is extremely forgiving. It simply tries to fill as many parameters as it can from whatever sources it can gather the information from. So, even if your particular route only passes id1, there's nothing stopping a devious user from tacking on ?id2=something along with it. If the model binder can use it, it will.




回答2:


I would like to show you another approach. It is maybe not as easy as you would like to have. But the crucial part is moved from declaration to code. So you can do more to tune your setting in.

In comparison with previous answer this solution is working, doing what you want. I am not discussing your intention, so please take it only as a working solution.

Firstly we will extend the declaration with a constraint and name the id parameters a bit exotically:

routes.MapRoute(
  name: "Default2",
  url: "{controller}/{action}/{theId1}-{theId2}",
  defaults: new { controller = "Home", action = "Index", theId1 = 0, theId2 = 0 },
  constraints: new { lang = new MyRouteConstraint() }
);

And now in the constraint we can extend the value collection (repeat our parameters)

public class MyRouteConstraint : IRouteConstraint
{
    public bool Match(System.Web.HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        // for now skip the Url generation
        if (routeDirection.Equals(RouteDirection.UrlGeneration))
        {
            return false;
        }

        // try to find out our parameters
        object theId1 = null;
        object theId2 = null;

        var parametersExist = values.TryGetValue("theId1", out theId1)
                            && values.TryGetValue("theId2", out theId2);

        // not our case
        if( ! parametersExist)
        {
            return false;
        }

        // first argument re-inserted
        values["id1"] = theId1;
        values["id3"] = theId1;
        // TODO add other, remove theId1

        // second argument re-inserted
        values["id2"] = theId2;
        values["id4"] = theId2;
        // TODO add other, remove theId2


        return true;
    }
}

This solution does not remove complexity of your url routing design decision. But it allows you to solve the problem in code, extendable way (e.g.reflection) as you need.




回答3:


Well, I decided to expand on Radim's idea and design my own custom route constraint (mentioned in this question.

Basically, instead of having many different routes, I now just have one route that matches anything with two integer parameters:

routes.MapRoute(
    name: "Default2",
    url: "{controller}/{action}/{param1}-{param2}",
    defaults: new { controller = "Admin", action = "Index" },
    constraints: new { lang = new CustomRouteConstraint(new RoutePatternCollection( new List<ParamType> { ParamType.INT, ParamType.INT })) }
);

In Application_Start() under Global.asax I set the controller namespace for the constraint (which is more efficient than trying to figure it out every time):

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    //should be called before RegisterRoutes
    CustomRouteConstraint.SetControllerNamespace("********.Controllers");

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);            
}

And finally the custom route constraint (I know it's a ton of code and probably overly complex but I think it's fairly self explanatory):

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web;
using System.Linq;
using System.Reflection;

namespace ********.Code
{
    public class CustomRouteConstraint : IRouteConstraint
    {
        private static string controllerNamespace;

        RoutePatternCollection patternCollection { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="CustomRouteConstraint"/> class.
        /// </summary>
        /// <param name="rPC">The route pattern collection to match.</param>
        public CustomRouteConstraint(RoutePatternCollection rPC)
        {
            this.patternCollection = rPC;

            if (string.IsNullOrWhiteSpace(controllerNamespace)) {
                controllerNamespace = Assembly.GetCallingAssembly().FullName.Split(new string[1] {","}, StringSplitOptions.None)
                    .FirstOrDefault().Trim().ToString();
            }
        }

        /// <summary>
        /// Sets the controller namespace. Should be called before RegisterRoutes.
        /// </summary>
        /// <param name="_namespace">The namespace.</param>
        public static void SetControllerNamespace(string _namespace)
        {
            controllerNamespace = _namespace;
        }

        /// <summary>
        /// Attempts to match the current request to an action with the constraint pattern.
        /// </summary>
        /// <param name="httpContext">The current HTTPContext of the request.</param>
        /// <param name="route">The route to which the constraint belongs.</param>
        /// <param name="paramName">Name of the parameter (irrelevant).</param>
        /// <param name="values">The url values to attempt to match.</param>
        /// <param name="routeDirection">The route direction (this method will ignore URL Generations).</param>
        /// <returns>True if a match has been found, false otherwise.</returns>
        public bool Match(HttpContextBase httpContext, Route route, string paramName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (routeDirection.Equals(RouteDirection.UrlGeneration)) {
                return false;
            }

            Dictionary<string, object> unMappedList = values.Where(x => x.Key.Contains("param")).OrderBy(xi => xi.Key).ToDictionary(
                kvp => kvp.Key, kvp => kvp.Value);

            string controller = values["controller"] as string;
            string action = values["action"] as string;

            Type cont = TryFindController(controller);

            if (cont != null) {
                MethodInfo actionMethod = cont.GetMethod(action);

                if (actionMethod != null) {
                    ParameterInfo[] methodParameters = actionMethod.GetParameters();

                    if (validateParameters(methodParameters, unMappedList)) {
                        for (int i = 0; i < methodParameters.Length; i++) {                            
                            var key = unMappedList.ElementAt(i).Key;
                            var value = values[key];

                            values.Remove(key);
                            values.Add(methodParameters.ElementAt(i).Name, value);
                        }

                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Validates the parameter lists.
        /// </summary>
        /// <param name="methodParameters">The method parameters for the found action.</param>
        /// <param name="values">The parameters from the RouteValueDictionary.</param>
        /// <returns>True if the parameters all match, false if otherwise.</returns>
        private bool validateParameters(ParameterInfo[] methodParameters, Dictionary<string, object> values)
        {
            //@TODO add flexibility for optional parameters
            if (methodParameters.Count() != patternCollection.parameters.Count()) {
                return false;
            }

            for (int i = 0; i < methodParameters.Length; i++) {
                if (!matchType(methodParameters[i], patternCollection.parameters.ElementAt(i), values.ElementAt(i).Value)) {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Matches the type of the found action parameter to the expected parameter, also attempts data conversion.
        /// </summary>
        /// <param name="actualParam">The actual parameter of the found action.</param>
        /// <param name="expectedParam">The expected parameter.</param>
        /// <param name="value">The value of the RouteValueDictionary corresponding to that parameter.</param>
        /// <returns>True if the parameters match, false if otherwise.</returns>
        private bool matchType(ParameterInfo actualParam, ParamType expectedParam, object value)
        {
            try {
                switch (expectedParam) {
                    case ParamType.BOOL:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.Boolean":
                                Convert.ToBoolean(value);
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    case ParamType.DOUBLE:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.Double":
                                Convert.ToDouble(value);
                                return true;
                                break;
                            case "System.Decimal":
                                Convert.ToDecimal(value);
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    case ParamType.INT:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.Int32":
                                Convert.ToInt32(value);
                                return true;
                                break;
                            case "System.Int16":
                                Convert.ToInt16(value);                                
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    case ParamType.LONG:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.Int64":
                                Convert.ToInt64(value);
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    case ParamType.STRING:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.String":
                                Convert.ToString(value);
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    case ParamType.UINT:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.UInt32":
                                Convert.ToUInt32(value);
                                return true;
                                break;
                            case "System.UInt16":
                                Convert.ToUInt16(value);
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    case ParamType.ULONG:
                        switch (actualParam.ParameterType.ToString()) {
                            case "System.UInt64":
                                Convert.ToUInt64(value);
                                return true;
                                break;
                            default:
                                return false;
                                break;
                        }
                        break;
                    default:
                        return false;
                }
            } catch (Exception) {
                return false;
            }
        }

        /// <summary>
        /// Attempts to discover a controller matching the one specified in the route.
        /// </summary>
        /// <param name="_controllerName">Name of the controller.</param>
        /// <returns>A System.Type containing the found controller, or null if the contoller cannot be discovered.</returns>
        private Type TryFindController(string _controllerName)
        {
            string controllerFullName;
            Assembly executingAssembly = Assembly.GetExecutingAssembly();

            if (!string.IsNullOrWhiteSpace(controllerNamespace)) {
                controllerFullName = string.Format(controllerNamespace + ".Controllers.{0}Controller", _controllerName);

                Type controller = executingAssembly.GetType(controllerFullName);

                if (controller == null) {
                    if (controllerNamespace.Contains("Controllers")) {
                        controllerFullName = string.Format(controllerNamespace + ".{0}Controller", _controllerName);

                        if ((controller = executingAssembly.GetType(controllerFullName)) == null) {
                            controllerFullName = string.Format(controllerNamespace + ".{0}", _controllerName);

                            controller = executingAssembly.GetType(controllerFullName);
                        }
                    } else {
                        controllerFullName = string.Format(controllerNamespace + "Controllers.{0}", _controllerName);

                        controller = executingAssembly.GetType(controllerFullName);
                    }
                }

                return controller;
            } else {
                controllerFullName = string.Format(Assembly.GetExecutingAssembly().FullName.Split(new string[1] {","}, StringSplitOptions.None)
                    .FirstOrDefault().Trim().ToString() + ".Controllers.{0}Controller", _controllerName);

                return Assembly.GetExecutingAssembly().GetType(controllerFullName);
            }            
        }
    }

    /// <summary>
    /// A list of the exepected parameters in the route.
    /// </summary>
    public struct RoutePatternCollection
    {
        public List<ParamType> parameters { get; set; }

        public RoutePatternCollection(List<ParamType> _params) : this()
        {
            this.parameters = _params;
        }
    }

    /// <summary>
    /// The valid parameter types for a Custom Route Constraint.
    /// </summary>
    public enum ParamType
    {
        STRING,
        INT,
        UINT,
        LONG,
        ULONG,
        BOOL,
        DOUBLE
    }
}

Feel free to suggest improvements if you see them!




回答4:


Don't worry about the parameter names in the controller methods. For example, "id" might represent a company id in one method and an account id in another. I have an application with a routing similar to this:

private const string IdRouteConstraint = @"^\d+$";

routes.MapRoute("TwoParameters",
   "{controller}/{action}/{categoryId}/{id}",
   new {controller = "Contents", action = "Index"},
   new {categoryId = IdRouteConstraint, id = IdRouteConstraint});

routes.MapRoute("OneParameter",
   "{controller}/{action}/{id}",
   new {controller = "Contents", action = "Index"},
   new {id = IdRouteConstraint});


来源:https://stackoverflow.com/questions/13382175/route-patterns-vs-individual-routes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!