If you've stumbled upon this post it probably means that you are dealing with a ASP.NET 4.x or ASP.NET Core MVC Controller containing two action methods that are conflicting, giving you the following error message:
The current request for action 'ActionName' on controller type 'ControllerName' is ambiguous between the following action methods: [...]
This most likely happens because you have declared two (or more) Action Methods with the same action name, such as the following:
1 2 3 |
public ActionResult SomeMethod(int someInt) { /* ... */ } public ActionResult SomeMethod(string someString) { /* ... */ } |
Unfortunately, MVC cannot support two actions with the same name... even with differents signatures. The only exception is when one of the actions is decorated with a different HTTP VERB attribute, like - for example - [HttpGet] for the former and [HttpPost] for the latter:
1 2 3 4 5 |
[HttpGet] public ActionResult SomeMethod(int someInt) { /* ... */ } [HttpPost] public ActionResult SomeMethod(string someString) { /* ... */ } |
This would indeed work... but what if we do want to work around it without having to change the HTTP VERB?
If we want to achieve such result, we have many alternatives:
- Rename one of them: SomeMethod(...) and SomeMethod2(...) will indeed (and obviously) work.
- Move one of them to a different Controller.
- Use the System.Web.Mvc.ActionNameAttribute to declare an overload alias, such as [ActionName("MyOverloadAliasName")] .
- Implement a custom attribute that will help the MVC request pipeline to understand with Action Method to use depending on the given parameters.
The latter option is by far the most interesting one: I got the idea from this StackOverflow thread a long time ago and I'm still using it - with some further enhancements, see below - in all my projects nowadays.
The first thing to do is to implement the attribute - we'll call it RequireParameterAttribute - in the following way:
1 2 3 4 5 6 7 8 9 |
public class RequireParameterAttribute : ActionMethodSelectorAttribute { public RequireParameterAttribute(string valueName) { ValueName = valueName; } public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { return (controllerContext.HttpContext.Request[ValueName] != null); } public string ValueName { get; private set; } } |
Then we can use it in the following way:
1 2 3 4 5 6 7 |
[HttpGet] [RequireParameter("someInt")] public ActionResult SomeMethod(int someInt) { /* ... */ } [HttpGet] [RequireParameter("someString")] public ActionResult SomeMethod(string someString) { /* ... */ } |
This will fix the issue and give you the chance to use multiple action methods with the same name, as long as they have different signatures.
Over the years, while working on many different MVC projects, I took the chance to further refine the RequiredParameterAttribute to make it support multiple parameters and different kind of matches that would trigger it - all of the given parameters, any one of them or even none.
Here's the updated class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/// <summary> /// Flags an Action Method valid for any incoming request only if all, any or none of the given HTTP parameter(s) are set, /// enabling the use of multiple Action Methods with the same name (and different signatures) within the same MVC Controller. /// </summary> public class RequireParameterAttribute : ActionMethodSelectorAttribute { public RequireParameterAttribute(string parameterName) : this(new[] { parameterName }) { } public RequireParameterAttribute(params string[] parameterNames) { ParameterNames = parameterNames; IncludeGET = true; IncludePOST = true; IncludeCookies = false; Mode = MatchMode.All; } public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { switch (Mode) { case MatchMode.All: default: return ( (IncludeGET && ParameterNames.All(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p))) || (IncludePOST && ParameterNames.All(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p))) || (IncludeCookies && ParameterNames.All(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p))) ); case MatchMode.Any: return ( (IncludeGET && ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p))) || (IncludePOST && ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p))) || (IncludeCookies && ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p))) ); case MatchMode.None: return ( (!IncludeGET || !ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p))) && (!IncludePOST || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p))) && (!IncludeCookies || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p))) ); } } public string[] ParameterNames { get; private set; } /// <summary> /// Set it to TRUE to include GET (QueryStirng) parameters, FALSE to exclude them: /// default is TRUE. /// </summary> public bool IncludeGET { get; set; } /// <summary> /// Set it to TRUE to include POST (Form) parameters, FALSE to exclude them: /// default is TRUE. /// </summary> public bool IncludePOST { get; set; } /// <summary> /// Set it to TRUE to include parameters from Cookies, FALSE to exclude them: /// default is FALSE. /// </summary> public bool IncludeCookies { get; set; } /// <summary> /// Use MatchMode.All to invalidate the method unless all the given parameters are set (default). /// Use MatchMode.Any to invalidate the method unless any of the given parameters is set. /// Use MatchMode.None to invalidate the method unless none of the given parameters is set. /// </summary> public MatchMode Mode { get; set; } public enum MatchMode : int { All, Any, None } } |
As we can see, it retains the old signature yet it also features an overload an a nice amount of configuration properties that can be used to further customize its behaviour.
Feel free to use it in your ASP.NET MVC projects!
Tried it but get
Value cannot be null.
Parameter name: source
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: source
Source Error:
Line 41: case MatchMode.All:
Line 42: default:
Line 43: return (
Line 44: (IncludeGET && ParameterNames.All(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
Line 45: || (IncludePOST && ParameterNames.All(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
It probably means that the ParameterNames property of the RequireParameterAttribute instance is NULL. Check that:
1) the class constructor contains the following line:
ParameterNames = parameterNames”;
2) you’re passing a valid string to the attribute:
[RequireParameter(“someParm”)]
It was great I used