ASP.NET MVC 2 and the Ambiguous Match Exception for Action methods with different signatures
Although you can use this technique to allow POST requests to a page avoiding a redirect this is now considered a bad practice from a usability perspective because if the user hits refresh they get the classic browser warning. You normally want to use a Post-Redirect-Get pattern: when you make a POST request, once the request completes you do a redirect so that a GET request is fired. In this way when the user refreshes the page, the last GET request will be executed rather than the POST. You can still use this technique to have two post methods with different parameters but the same name, but why bother if each is going to do a redirect anyway back to the page Action method?
One frustration I have with ASP.NET MVC is that you can't easily have
two actions with the same name but with different parameters, e.g.
Index(int a, int b)
, and Index (int a)
. If you try this you will get
an AmbiguousMatchException
because it makes no attempt to match the
form values with the method parameters to figure out which method you
want to call. Now you can decorate the Index()
with
[AcceptVerbs(HttpVerbs.Get)]
so that it at least will not be competing
for ASP.NET MVC's attention during the form post but your other two
index methods will still cause the exception.
Supposed you wanted a page /Home/Index that had two forms on it:-
<%using (Html.BeginForm()) { %> <%=Html.TextBox("a") %> <input type="submit" name="submitOne" title="click me" /> <%} %>
<%using (Html.BeginForm()) { %> <%=Html.TextBox("a") %>
<%=Html.TextBox("b") %> <input type="submit" name="submitTwo" title="click me" /> <%} %>
What we'd like to do is be able to have three action methods, Index()
, Index(Int a)
and Index (Int a, Int b)
.
So let's define our action methods like that and add a filter attribute to them that will filter methods according to the posted values ignoring any for which there aren't enough posted values to match the number of parameters or for which the parameter names don't match.
/// <summary>
/// Post a single integer back to the form, but don't allow url /Home/Index/23
/// </summary>
[ParametersMatch]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index([FormValue]int a)
{
ViewData["Message"] = "You supplied one value " + a ;
return View();
}
/// <summary>
/// Post two integers back to the form OR include two integers in the path
/// </summary>
[ParametersMatch]
[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Get)]
public ActionResult Index([FormValue]int a, [FormValue]int b)
{
ViewData["Message"] = "You supplied two values " + a + " " + b;
return View();
}
And finally, here's the code that makes that possible: an attribute you can apply to a method parameter to indicate that you want it in the posted form, and an action filter that filters out any action methods that don't match.
With this in place you can (i) avoid an unnecessary redirect and (ii) have actions with the same name but with different parameters.
Create a new ActionFilter like this ...
namespace TestApplication.Controllers
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Web.Mvc.Resources;
using System.Web.Mvc;
using System.Diagnostics;
/// <summary>
/// This attribute can be placed on a parameter of an action method that should be present on the URL in route data
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple=false)]
public sealed class RouteValueAttribute : Attribute
{
public RouteValueAttribute() { }
}
/// <summary>
/// This attribute can be placed on a parameter of an action method that should be present in FormData
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class FormValueAttribute : Attribute
{
public FormValueAttribute() { }
}
/// <summary>
/// Parameters Match Attribute allows you to specify that an action is only valid
/// if it has the right number of parameters marked [RouteValue] or [FormValue] that match with the form data or route data
/// </summary>
/// <remarks>
/// This attribute allows you to have two actions with the SAME name distinguished by the values they accept according to the
/// name of those values. Does NOT handle complex types and bindings yet but could be easily adapted to do so.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ParametersMatchAttribute : ActionMethodSelectorAttribute
{
public ParametersMatchAttribute() { }
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
// The Route values
List<string> requestRouteValuesKeys = controllerContext.RouteData.Values.Where(v => !(v.Key == "controller" || v.Key == "action" || v.Key == "area")).Select(rv => rv.Key).ToList();
// The Form values
var form = controllerContext.HttpContext.Request.Form;
List<string> requestFormValuesKeys = form.AllKeys.ToList();
// The parameters this method expects
var parameters = methodInfo.GetParameters();
// Parameters from the method that we haven't matched up against yet
var parametersNotMatched = parameters.ToList();
// each parameter of the method can be marked as a [RouteValue] or [FormValue] or both or nothing
foreach (var param in parameters)
{
string name = param.Name;
bool isRouteParam = param.GetCustomAttributes(true).Any(a => a is RouteValueAttribute);
bool isFormParam = param.GetCustomAttributes(true).Any(a => a is FormValueAttribute);
if (isRouteParam && requestRouteValuesKeys.Contains(name))
{
// Route value matches parameter
requestRouteValuesKeys.Remove(name);
parametersNotMatched.Remove(param);
}
else if (isFormParam && requestFormValuesKeys.Contains(name))
{
// Form value matches method parameter
requestFormValuesKeys.Remove(name);
parametersNotMatched.Remove(param);
}
else
{
// methodInfo parameter does not match a route value or a form value
Debug.WriteLine(methodInfo + " failed to match " + param + " against either a RouteValue or a FormValue");
return false;
}
}
// Having removed all the parameters of the method that are matched by either a route value or a form value
// we are now left with all the parameters that do not match and all the route and form values that were not used
if (parametersNotMatched.Count > 0)
{
Debug.WriteLine(methodInfo + " - FAIL: has parameters left over not matched by route or form values");
return false;
}
if (requestRouteValuesKeys.Count > 0)
{
Debug.WriteLine("... at aren't consumed");
return false;
}
if (requestFormValuesKeys.Count > 1)
{
Debug.WriteLine(methodInfo + " - FAIL : unmatched form values " + string.Join(", ", requestFormValuesKeys.ToArray()));
return false;
}
Debug.WriteLine(methodInfo + " - PASS - unmatched form values " + string.Join(", ", requestFormValuesKeys.ToArray()));
return true;
}
}