Skip to content

Custom Action method overloading in MVC3

February 3, 2012

In one of the MVC3 apps i’ve been working on we decided to make sure that all of the urls specify information in a restful fashion, that is they are all directly in the path(route), nothing in request parameters.  Everything was going smoothly until we hit our first action where parameters are optional.

Lets look at this theoretical route table:

"{controller}/{action}/{year}/team/{teamId}/car/{carId}/track/{trackId}"
"{controller}/{action}/{year}/track/{trackId}/car/{carId}"
"{controller}/{action}/{year}/car/{carId}"

So you could imagine that we would have different urls that apply to these routes like:

http://www.racingstats.com/yearlySummary/Nascar/2011/team/AmsOil/car/12
http://www.racingstats.com/yearlySummary/Nascar/2011/track/taladega/car/12
http://www.racingstats.com/yearlySummary/Nascar/2011/car/12

The problem is that this url pattern isn’t gracefully handled out of the box with mvc3.  If you tried to hit one you would be greated with a nice exception:

The current request for action ‘Nascar’ on controller type ‘YearlySummaryController’ is ambiguous between the following action methods:….

The reason is that we are specifying the same controller in all 3 (yearlySummary) and the same action (Nascar) in all 3, but with a different set of parameters for all.  The only way you can handle it is to have a single action called “Nascar” that takes every single parameter possible. 

public ActionResult Nascar(int? year, string teamId, int? carId, string trackId)

So based on whatever combination of values either have or do not have a value you will need to determine what behaviour to execute.  You can see that as the number of parameters grows this gets more and more cumbersome and complex.  The worst part of all is that you are using your action to do routing, after you have already gone to the trouble of building a route table! 

We need to do this because the MVC3 routing system does not discern actions based on their parameters, it only differentiates them by name, or by a couple of extended attributes you can apply.

The good news is that they provided a couple of abstract classes you can extend to let you apply filters on action methods as you see fit so you can have the framework pick the appropriate action method however you like.  I’m going to focus on the System.Web.Mvc.ActionMethodSelectorAttribute abstract class.  This class implements only one abstract method.

public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo);

So when you implement this method in your custom Action Method Selector you get to work with the current controllerContext which includes information like the current request, route value parameters, etc.  You also get the MethodInfo instance for the method that the selector is currently being applied to.

Lets say for the example above, instead of having a single action method that takes in all parameters we want to have a separate action method for each combination of parameters we are passing in.

public ActionResult Nascar(int? year, string teamId, int? carId, string trackId)
public ActionResult Nascar(int? year, string trackId, int? carId)
public ActionResult Nascar(int? year, int? carId)

So we can differentiate them by parameter names as they all have a unique combination.  Using the abstract class specified above we have access to all the information required to do this.

Through the methodInfo parameter we can get access to all of the parameter names of the current method:

methodInfo.GetParameters()

And through the controllerContext parameter we can get access to all of the route values that were gathered from the request path:

controllerContext.RouteData.Values

The RouteData values are essentially just a dictionary of key/value pairs constituting all of the information from the path.  The one thing you do need to do here is filter out all of the information that you are not interested in.  At the most basic level you need to filter out the “controller”, “action”, and “area” values as those are not used as parameter values for our method.  In some cases the framework also adds other keys with objects such as a “DictionaryValueProvider” which is another thing we are not concerned with.  I handled it by simply ignoring any key that has a value which is not a simple type, and also excluding the 3 keys listed above.

Once you have the route values that you will be passing to a method as parameters, you can just match up the route value keys to the parameter names and only return true if there is a 1:1 match for each value. 

This method has worked well in our project so far.  The obvious limitation here is that you still can’t overload a method with the same number and type of parameters, no amount of MVC magic will help you there.  You could possibly use it in combination with the "[ActionName()]” attribute to use the same action name on a different method but I haven’t tried it.

Enough with the details, i’m sure you all came here for the code Smile

public class MatchParametersOnRouteKeys : ActionMethodSelectorAttribute
    {
        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
        {
            var methodParams = methodInfo.GetParameters();
            List<string> passedValues = GetReleventRouteDataValues(controllerContext.RouteData.Values);

            return CheckIfPassedValuesMatchMethodParameters(passedValues, methodParams);
        }

        public bool CheckIfPassedValuesMatchMethodParameters(List<string> passedValues, ParameterInfo[] methodParams)
        {
            if (methodParams.Length != passedValues.Count)
                return false;

            return methodParams.All(t => passedValues.Contains(t.Name.ToLower()));
        }

        public List<string> GetReleventRouteDataValues(RouteValueDictionary routeVals)
        {
            return routeVals.Where(v => IsNotTheActionOrController(v) && IsASimpleType(v)).Select(rv => rv.Key.ToLower()).ToList();
        }

        private static bool IsASimpleType(KeyValuePair<string, object> v)
        {
            return (v.Value.GetType().IsPrimitive || v.Value is String);
        }

        private static bool IsNotTheActionOrController(KeyValuePair<string, object> v)
        {
            return v.Key != "action" && v.Key != "controller" && v.Key != "area";
        }
    }

So once you add this to your solution you just decorate your action methods like so:

[MatchParametersOnRouteKeys]
public ActionResult Nascar(int? year, string trackId, int? carId)

Happy overloading!

Advertisements

From → Uncategorized

4 Comments
  1. Muhammad Usman permalink

    can you Have know issue like that we have different action names with same controller but parameters where same and the action we call runs multiple time.

    like,

    we have routes

    routes.MapRoute(
    “DeleteQuestion”, // Route name
    “{controller}/{action}/{id}/{param1}”, // URL with parameters
    new { controller = “Document”, action = “deleteQuestion”, id = UrlParameter.Optional, param1 = UrlParameter.Optional } // Parameter defaults
    );
    routes.MapRoute(
    “UpdateQuestion”, // Route name
    “{controller}/{action}/{id}/{param1}”, // URL with parameters
    new { controller = “Document”, action = “updateQuestion”, id = UrlParameter.Optional, param1 = UrlParameter.Optional } // Parameter defaults
    );
    routes.MapRoute(
    “DeleteQuestionPage”, // Route name
    “{controller}/{action}/{id}/{param1}”, // URL with parameters
    new { controller = “Document”, action = “deleteQuestionPage”, id = UrlParameter.Optional, param1 = UrlParameter.Optional } // Parameter defaults
    );

    routes.MapRoute(
    “SaveUserResponseForQuestions”, // Route name
    “{controller}/{action}/{id}/{param1}”, // URL with parameters
    new { controller = “Document”, action = “saveUserResponseForQuestions”, id = UrlParameter.Optional, param1 = UrlParameter.Optional } // Parameter defaults
    );
    routes.MapRoute(
    “DisplayQuestion”, // Route name
    “{controller}/{action}/{id}/{param1}”, // URL with parameters
    new { controller = “Document”, action = “displayQuestion”, id = UrlParameter.Optional, param1 = UrlParameter.Optional } // Parameter defaults

    );

    now When i call

    htttp://mydomain/document/displayQuestion/1/1

    this action(ie. displayquestion) runs multiple time(actually 5 times) in which when it runs first time get exact result but in second and so on it will get 0 in second parameter

  2. mdchris permalink

    You only need to define 1 route because the route format is always the same in the examples you provieded. When you specify the controller, action, and parameter in the route all that does is set the default values for if none are specified, but when one is specified in the url it will use that one.

    When you load htttp://mydomain/document/displayQuestion/1/1 it matches to all 5 routes you defined and that’s why it runs 5 times.

  3. Thanks Chris… Keep the work coming..

Trackbacks & Pingbacks

  1. Developing Code, Connecting Industry

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: